# ------------------------------------------------------------------------------
# Name: features.jSymbolic.py
# Purpose: music21 functions for simple feature extraction
#
# Authors: Christopher Ariza
#
# Copyright: Copyright © 2011 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
The features implemented here are based on those found in jSymbolic and
defined in Cory McKay's MA Thesis, "Automatic Genre Classification of MIDI Recordings".
Type annotations in this module were added with AI assistance (Claude).
'''
from __future__ import annotations
from collections import OrderedDict
from collections.abc import Sequence
import copy
import math
from math import isclose
import statistics
from textwrap import dedent
import unittest
from music21 import base
from music21 import environment
from music21 import exceptions21
from music21.features import base as featuresModule
from music21.instrument import Instrument
environLocal = environment.Environment('features.jSymbolic')
# ------------------------------------------------------------------------------
# 112 feature extractors
# ------------------------------------------------------------------------------
# melody
[docs]
class MelodicIntervalHistogramFeature(featuresModule.FeatureExtractor):
'''
A features array with bins corresponding to the values of the melodic interval histogram.
128 dimensions
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MelodicIntervalHistogramFeature(s)
>>> f = fe.extract()
>>> f.vector[0:5]
[0.144..., 0.220..., 0.364..., 0.062..., 0.050...]
'''
id = 'M1'
name = 'Melodic Interval Histogram'
description = ('A features array with bins corresponding to '
'the values of the melodic interval histogram.')
dimensions = 128
normalize = True
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
for i, value in enumerate(self.data['midiIntervalHistogram']):
self.feature.vector[i] = value
[docs]
class AverageMelodicIntervalFeature(featuresModule.FeatureExtractor):
'''
Average melodic interval (in semitones).
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.AverageMelodicIntervalFeature(s)
>>> f = fe.extract()
>>> f.vector
[2.44...]
'''
id = 'M2'
name = 'Average Melodic Interval'
description = 'Average melodic interval (in semitones).'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
values = []
# already summed by part if parts exist
histo = self.data['midiIntervalHistogram']
for i, value in enumerate(histo):
for j in range(value):
values.append(i)
if not values:
raise JSymbolicFeatureException('input lacks notes')
self.feature.vector[0] = sum(values) / len(values)
[docs]
class MostCommonMelodicIntervalFeature(featuresModule.FeatureExtractor):
'''
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MostCommonMelodicIntervalFeature(s)
>>> f = fe.extract()
>>> f.vector
[2]
'''
id = 'M3'
name = 'Most Common Melodic Interval'
description = 'Melodic interval with the highest frequency.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# already summed by part if parts exist
histo = self.data['midiIntervalHistogram']
maxValue = max(histo)
maxIndex = histo.index(maxValue)
self.feature.vector[0] = maxIndex
[docs]
class DistanceBetweenMostCommonMelodicIntervalsFeature(
featuresModule.FeatureExtractor):
'''
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.DistanceBetweenMostCommonMelodicIntervalsFeature(s)
>>> f = fe.extract()
>>> f.vector
[1]
'''
id = 'M4'
name = 'Distance Between Most Common Melodic Intervals'
description = ('Absolute value of the difference between the '
'most common melodic interval and the second most '
'common melodic interval.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# copy b/c will manipulate
histo = copy.deepcopy(self.data['midiIntervalHistogram'])
maxValue = max(histo)
maxIndex = histo.index(maxValue)
histo[maxIndex] = 0 # set to zero
secondValue = max(histo)
secondIndex = histo.index(secondValue)
self.feature.vector[0] = abs(maxIndex - secondIndex)
[docs]
class MostCommonMelodicIntervalPrevalenceFeature(
featuresModule.FeatureExtractor):
'''
Fraction of melodic intervals that belong to the most common interval.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MostCommonMelodicIntervalPrevalenceFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.364...]
'''
id = 'M5'
name = 'Most Common Melodic Interval Prevalence'
description = 'Fraction of melodic intervals that belong to the most common interval.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# copy b/c will manipulate
histo = copy.deepcopy(self.data['midiIntervalHistogram'])
maxValue = max(histo)
count = sum(histo)
if not count:
raise JSymbolicFeatureException('input lacks notes')
self.feature.vector[0] = maxValue / count
[docs]
class RelativeStrengthOfMostCommonIntervalsFeature(
featuresModule.FeatureExtractor):
'''
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.RelativeStrengthOfMostCommonIntervalsFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.603...]
'''
id = 'M6'
name = 'Relative Strength of Most Common Intervals'
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.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# copy b/c will manipulate
histo = copy.deepcopy(self.data['midiIntervalHistogram'])
count = sum(histo)
maxValue = max(histo)
maxIndex = histo.index(maxValue)
histo[maxIndex] = 0 # set to zero
secondValue = max(histo)
# secondIndex = histo.index(secondValue)
if not count:
raise JSymbolicFeatureException('input lacks notes')
self.feature.vector[0] = (secondValue / count) / (maxValue / count)
[docs]
class NumberOfCommonMelodicIntervalsFeature(featuresModule.FeatureExtractor):
'''
Number of melodic intervals that represent at least 9% of all melodic intervals.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.NumberOfCommonMelodicIntervalsFeature(s)
>>> f = fe.extract()
>>> f.vector
[3]
'''
id = 'M7'
name = 'Number of Common Melodic Intervals'
description = ('Number of melodic intervals that represent '
'at least 9% of all melodic intervals.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['midiIntervalHistogram']
total = sum(histo)
if not total:
raise JSymbolicFeatureException('input lacks notes')
post = 0
for i, count in enumerate(histo):
if count / total >= 0.09:
post += 1
self.feature.vector[0] = post
[docs]
class AmountOfArpeggiationFeature(featuresModule.FeatureExtractor):
'''
Fraction of horizontal intervals that are repeated notes, minor thirds, major thirds,
perfect fifths, minor sevenths, major sevenths, octaves, minor tenths or major tenths.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.AmountOfArpeggiationFeature(s)
>>> f = fe.extract()
>>> f.name
'Amount of Arpeggiation'
>>> f.vector
[0.333...]
'''
id = 'M8'
name = 'Amount of Arpeggiation'
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.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['midiIntervalHistogram']
total = sum(histo)
if total == 0:
return # do nothing
# intervals to look for
targets = [0, 3, 4, 7, 10, 11, 12, 15, 16]
total = sum(histo)
count = 0
for t in targets:
count += histo[t]
if not count:
raise JSymbolicFeatureException('input lacks notes')
self.feature.vector[0] = count / total
[docs]
class RepeatedNotesFeature(featuresModule.FeatureExtractor):
'''
Fraction of notes that are repeated melodically.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.RepeatedNotesFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.144...]
'''
id = 'M9'
name = 'Repeated Notes'
description = 'Fraction of notes that are repeated melodically.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['midiIntervalHistogram']
total = sum(histo)
if total == 0:
return # do nothing
# intervals to look for
targets = [0]
total = sum(histo)
if not total:
raise JSymbolicFeatureException('input lacks notes')
count = 0
for t in targets:
count += histo[t]
self.feature.vector[0] = count / total
[docs]
class ChromaticMotionFeature(featuresModule.FeatureExtractor):
'''
Fraction of melodic intervals corresponding to a semitone.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.ChromaticMotionFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.220...]
'''
id = 'm10'
name = 'Chromatic Motion'
description = 'Fraction of melodic intervals corresponding to a semi-tone.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['midiIntervalHistogram']
total = sum(histo)
if not total:
raise JSymbolicFeatureException('input lacks notes')
# intervals to look for
targets = [1]
count = 0
for t in targets:
count += histo[t]
self.feature.vector[0] = count / total
[docs]
class StepwiseMotionFeature(featuresModule.FeatureExtractor):
'''
Fraction of melodic intervals that correspond to a minor or major second.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.StepwiseMotionFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.584...]
'''
id = 'M11'
name = 'Stepwise Motion'
description = ('Fraction of melodic intervals that corresponded '
'to a minor or major second.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['midiIntervalHistogram']
total = sum(histo)
if not total:
raise JSymbolicFeatureException('input lacks notes')
# intervals to look for
targets = [1, 2]
count = 0
for t in targets:
count += histo[t]
self.feature.vector[0] = count / total
[docs]
class MelodicThirdsFeature(featuresModule.FeatureExtractor):
'''
Fraction of melodic intervals that are major or minor thirds.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MelodicThirdsFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.113...]
'''
id = 'M12'
name = 'Melodic Thirds'
description = 'Fraction of melodic intervals that are major or minor thirds.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['midiIntervalHistogram']
total = sum(histo)
if not total:
raise JSymbolicFeatureException('input lacks notes')
# intervals to look for
targets = [3, 4]
count = 0
for t in targets:
count += histo[t]
self.feature.vector[0] = count / total
[docs]
class MelodicFifthsFeature(featuresModule.FeatureExtractor):
'''
Fraction of melodic intervals that are perfect fifths.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MelodicFifthsFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.056...]
'''
id = 'M13'
name = 'Melodic Fifths'
description = 'Fraction of melodic intervals that are perfect fifths.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['midiIntervalHistogram']
total = sum(histo)
if not total:
raise JSymbolicFeatureException('input lacks notes')
# intervals to look for
targets = [7]
count = 0
for t in targets:
count += histo[t]
self.feature.vector[0] = count / total
[docs]
class MelodicTritonesFeature(featuresModule.FeatureExtractor):
'''
Fraction of melodic intervals that are tritones.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MelodicTritonesFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.012...]
'''
id = 'M14'
name = 'Melodic Tritones'
description = 'Fraction of melodic intervals that are tritones.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['midiIntervalHistogram']
total = sum(histo)
if not total:
raise JSymbolicFeatureException('input lacks notes')
# intervals to look for
targets = [6]
count = 0
for t in targets:
count += histo[t]
self.feature.vector[0] = count / total
[docs]
class MelodicOctavesFeature(featuresModule.FeatureExtractor):
'''
Fraction of melodic intervals that are octaves.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MelodicOctavesFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.018...]
'''
id = 'M15'
name = 'Melodic Octaves'
description = 'Fraction of melodic intervals that are octaves.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['midiIntervalHistogram']
total = sum(histo)
if not total:
raise JSymbolicFeatureException('input lacks notes')
# intervals to look for
targets = [12, 24, 48, 60, 72, 84, 96, 108, 120]
count = 0
for t in targets:
count += histo[t]
self.feature.vector[0] = count / total
[docs]
class DirectionOfMotionFeature(featuresModule.FeatureExtractor):
'''
Returns the fraction of melodic intervals that are rising rather than falling.
Unisons are omitted.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.DirectionOfMotionFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.470...]
'''
id = 'm17'
name = 'Direction of Motion'
description = 'Fraction of melodic intervals that are rising rather than falling.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
rising = 0
falling = 0
cBundle = []
if self.data.partsCount > 0:
for i in range(self.data.partsCount):
cList = self.data['parts'][i]['contourList']
cBundle.append(cList)
else:
cList = self.data['contourList']
cBundle.append(cList)
for cList in cBundle:
for c in cList:
if c > 0:
rising += 1
elif c < 0:
falling += 1
if not (falling or rising):
raise JSymbolicFeatureException('input lacks notes')
self.feature.vector[0] = rising / (falling + rising)
[docs]
class DurationOfMelodicArcsFeature(featuresModule.FeatureExtractor):
'''
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.
Example: C D E D C D E C C
Intervals: [0] 2 2 -2 -2 2 2 -4 0
Changes direction (equivalent to +/- sign) three times.
There are seven non-unison (nonzero) intervals.
Thus, the duration of arcs is 7/3 ~= 2.333...
>>> s = converter.parse("tinyNotation: c' d' e' d' c' d' e'2 c'2 c'2")
>>> fe = features.jSymbolic.DurationOfMelodicArcsFeature(s)
>>> fe.extract().vector
[2.333...]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.DurationOfMelodicArcsFeature(s)
>>> fe.extract().vector
[1.74...]
'''
id = 'M18'
name = 'Duration of Melodic Arcs'
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.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# `cList` contains a list of melodic intervals in a part.
# For example, C4 E4 G4 E4 C4 results in a cList of [4, 3, -3, -4].
# Each part is encoded in a separate cList; cBundle contains all
# the cList arrays.
cBundle = []
if self.data.partsCount > 0:
for i in range(self.data.partsCount):
cList = self.data['parts'][i]['contourList']
cBundle.append(cList)
else:
cList = self.data['contourList']
cBundle.append(cList)
direction_changes = 0
nonUnison_intervals = 0
# For each part, count how many times the direction changes
# by looking at the sign of the interval.
ASCENDING = 1
DESCENDING = -1
STATIONARY = 0
for cList in cBundle:
current_direction = STATIONARY
for interval in cList:
if interval != 0:
nonUnison_intervals += 1
if current_direction == ASCENDING:
if interval < 0:
direction_changes += 1
current_direction = DESCENDING
elif current_direction == DESCENDING:
if interval > 0:
direction_changes += 1
current_direction = ASCENDING
else: # if we begin stationary
if interval > 0:
current_direction = ASCENDING
elif interval < 0:
current_direction = DESCENDING
# Duration of melodic arcs is 0 if it never changes direction
duration_of_melodic_arcs: float
if direction_changes == 0:
duration_of_melodic_arcs = 0
else:
duration_of_melodic_arcs = nonUnison_intervals / direction_changes
self.feature.vector[0] = duration_of_melodic_arcs
[docs]
class SizeOfMelodicArcsFeature(featuresModule.FeatureExtractor):
'''
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 of melodic arcs is defined as the
total size of melodic intervals between changes of directions -
or between the start of the melody and the first change of
direction - divided by the number of direction changes.
Example: C D E D C E D C C
Intervals: [0] 2 2 -2 -2 2 2 -4 0
Changes direction (equivalent to +/- sign) three times.
The total sum of interval distance up to the last change
of direction is 12. We don't count the last interval,
the descending major third, because it is not between
changes of direction.
Thus, the average size of melodic arcs is 12/3 = 4.
>>> s = converter.parse("tinyNotation: c' d' e' d' c' d' e'2 c'2 c'2")
>>> fe = features.jSymbolic.SizeOfMelodicArcsFeature(s)
>>> fe.extract().vector
[4.0]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.SizeOfMelodicArcsFeature(s)
>>> fe.extract().vector
[4.84...]
'''
id = 'M19'
name = 'Size of Melodic Arcs'
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 of'
'melodic arcs is defined as the total size of melodic'
'intervals between changes of directions - or between'
'the start of the melody and the first change of'
'direction - divided by the number of direction changes.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# `cList` contains a list of melodic intervals in a part.
# For example, C4 E4 G4 E4 C4 results in a cList of [4, 3, -3, -4].
# Each part is encoded in a separate cList; cBundle contains all
# the cList arrays.
cBundle = []
if self.data.partsCount > 0:
for i in range(self.data.partsCount):
cList = self.data['parts'][i]['contourList']
cBundle.append(cList)
else:
cList = self.data['contourList']
cBundle.append(cList)
direction_changes = 0
sum_of_intervals = 0
# For each part, count how many times the direction changes
# by looking at the sign of the interval.
ASCENDING = 1
DESCENDING = -1
STATIONARY = 0
for cList in cBundle:
current_direction = STATIONARY
this_arc_interval = 0
for interval in cList:
if current_direction == ASCENDING:
if interval > 0:
this_arc_interval += abs(interval)
elif interval < 0:
# total interval before the change gets added
sum_of_intervals += this_arc_interval
direction_changes += 1
current_direction = DESCENDING
# start fresh with the new arc on this interval
this_arc_interval = abs(interval)
elif current_direction == DESCENDING:
if interval < 0:
this_arc_interval += abs(interval)
elif interval > 0:
# total interval before the change gets added
sum_of_intervals += this_arc_interval
direction_changes += 1
current_direction = ASCENDING
# start fresh with the new arc on this interval
this_arc_interval = abs(interval)
else: # if we begin stationary
if interval > 0:
current_direction = ASCENDING
this_arc_interval += abs(interval)
elif interval < 0:
current_direction = DESCENDING
this_arc_interval += abs(interval)
# If it never changes direction, the size of melodic arcs is defined to be 0
size_of_melodic_arcs: float
if direction_changes == 0:
size_of_melodic_arcs = 0
else:
size_of_melodic_arcs = sum_of_intervals / direction_changes
self.feature.vector[0] = size_of_melodic_arcs
# ------------------------------------------------------------------------------
# pitch
[docs]
class MostCommonPitchPrevalenceFeature(featuresModule.FeatureExtractor):
'''
Fraction of Notes corresponding to the most common pitch.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MostCommonPitchPrevalenceFeature(s)
>>> fe.extract().vector[0]
0.116...
'''
id = 'P1'
name = 'Most Common Pitch Prevalence'
description = 'Fraction of Note Ons corresponding to the most common pitch.'
discrete = False
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
if not histo:
raise JSymbolicFeatureException('input lacks notes')
# if a tie this will return the first
# if all zeros will return zero
pcMax = max(histo.values())
pcCount = sum(histo.values())
# the number of the max divided by total for all
self.feature.vector[0] = pcMax / pcCount
[docs]
class MostCommonPitchClassPrevalenceFeature(featuresModule.FeatureExtractor):
'''
Fraction of Notes corresponding to the most common pitch class.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MostCommonPitchClassPrevalenceFeature(s)
>>> fe.extract().vector
[0.196...]
'''
id = 'P2'
name = 'Most Common Pitch Class Prevalence'
description = 'Fraction of Note Ons corresponding to the most common pitch class.'
discrete = False
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.pitchClassHistogram']
# if a tie this will return the first
# if all zeros will return zero
pc = histo.index(max(histo))
pcCount = sum(histo)
if not pcCount:
raise JSymbolicFeatureException('input lacks notes')
# the number of the max divided by total for all
self.feature.vector[0] = histo[pc] / pcCount
[docs]
class RelativeStrengthOfTopPitchesFeature(featuresModule.FeatureExtractor):
'''
The frequency of the 2nd most common pitch divided by the frequency of the most common pitch.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.RelativeStrengthOfTopPitchesFeature(s)
>>> fe.extract().vector
[0.947...]
'''
id = 'P3'
name = 'Relative Strength of Top Pitches'
description = ('The frequency of the 2nd most common pitch '
'divided by the frequency of the most common pitch.')
discrete = False
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
# if a tie this will return the first
# if all zeros will return zero
try:
pMax, pSecond = histo.most_common(2)[:2] # need [:2] in case of ties
self.feature.vector[0] = float(pSecond[1] / pMax[1])
except ZeroDivisionError:
raise JSymbolicFeatureException('input lacks notes')
except (IndexError, ValueError):
self.feature.vector[0] = 0.0
[docs]
class RelativeStrengthOfTopPitchClassesFeature(featuresModule.FeatureExtractor):
'''
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.RelativeStrengthOfTopPitchClassesFeature(s)
>>> fe.extract().vector
[0.906...]
'''
id = 'P4'
name = 'Relative Strength of Top Pitch Classes'
description = ('The frequency of the 2nd most common pitch class '
'divided by the frequency of the most common pitch class.')
discrete = False
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# copy b/c will edit
histo = copy.deepcopy(self.data['pitches.pitchClassHistogram'])
# if a tie this will return the first
# if all zeros will return zero
pIndexMax = histo.index(max(histo))
pCountMax = histo[pIndexMax]
if not pCountMax:
raise JSymbolicFeatureException('input lacks notes')
# set that position to zero and find next max
histo[pIndexMax] = 0
pIndexSecond = histo.index(max(histo))
pCountSecond = histo[pIndexSecond]
# the number of the max divided by total for all
self.feature.vector[0] = pCountSecond / pCountMax
[docs]
class IntervalBetweenStrongestPitchesFeature(featuresModule.FeatureExtractor):
'''
Absolute value of the difference between the pitches of the two most common MIDI pitches.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.IntervalBetweenStrongestPitchesFeature(s)
>>> fe.extract().vector
[5]
'''
id = 'P5'
name = 'Interval Between Strongest Pitches'
description = ('Absolute value of the difference between '
'the pitches of the two most common MIDI pitches.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
# if a tie this will return the first
# if all zeros will return zero
try:
pMax, pSecond = histo.most_common(2)[:2] # need [:2] in case of ties
self.feature.vector[0] = abs(pSecond[0] - pMax[0])
except (IndexError, ValueError):
self.feature.vector[0] = 0.0
[docs]
class IntervalBetweenStrongestPitchClassesFeature(
featuresModule.FeatureExtractor):
'''
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.IntervalBetweenStrongestPitchClassesFeature(s)
>>> fe.extract().vector
[5]
'''
id = 'P6'
name = 'Interval Between Strongest Pitch Classes'
description = ('Absolute value of the difference between the pitch '
'classes of the two most common MIDI pitch classes.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = copy.deepcopy(self.data['pitches.pitchClassHistogram'])
# if a tie this will return the first
# if all zeros will return zero
pIndexMax = histo.index(max(histo))
# set that position to zero and find next max
histo[pIndexMax] = 0
pIndexSecond = histo.index(max(histo))
# the number of the max divided by total for all
self.feature.vector[0] = abs(pIndexMax - pIndexSecond)
[docs]
class NumberOfCommonPitchesFeature(featuresModule.FeatureExtractor):
'''
Number of pitches that account individually for at least 9% of all notes.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.NumberOfCommonPitchesFeature(s)
>>> fe.extract().vector
[3]
'''
id = 'P7'
name = 'Number of Common Pitches'
description = ('Number of pitches that account individually '
'for at least 9% of all notes.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
total = sum(histo.values())
post = 0
for count in histo.values():
if count / total >= 0.09:
post += 1
self.feature.vector[0] = post
[docs]
class PitchVarietyFeature(featuresModule.FeatureExtractor):
'''
Number of pitches used at least once.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.PitchVarietyFeature(s)
>>> fe.extract().vector
[24]
'''
id = 'P8'
name = 'Pitch Variety'
description = 'Number of pitches used at least once.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
post = 0
for i, count in enumerate(histo):
if count >= 1:
post += 1
self.feature.vector[0] = post
[docs]
class PitchClassVarietyFeature(featuresModule.FeatureExtractor):
'''
Number of pitch classes used at least once.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.PitchClassVarietyFeature(s)
>>> fe.extract().vector
[10]
'''
id = 'P9'
name = 'Pitch Class Variety'
description = 'Number of pitch classes used at least once.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.pitchClassHistogram']
post = 0
for i, count in enumerate(histo):
if count >= 1:
post += 1
self.feature.vector[0] = post
[docs]
class RangeFeature(featuresModule.FeatureExtractor):
'''
Difference between highest and lowest pitches, in semitones.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.RangeFeature(s)
>>> fe.extract().vector
[34]
'''
id = 'P10'
name = 'Range'
description = 'Difference between highest and lowest pitches.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
if not histo:
raise JSymbolicFeatureException('input lacks notes')
minIndex = min(histo.keys())
maxIndex = max(histo.keys())
self.feature.vector[0] = maxIndex - minIndex
[docs]
class MostCommonPitchFeature(featuresModule.FeatureExtractor):
'''
Bin label of the most common pitch.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MostCommonPitchFeature(s)
>>> fe.extract().vector
[61]
'''
id = 'P11'
name = 'Most Common Pitch'
description = 'Bin label of the most common pitch.'
discrete = False
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
try:
pNumberMax = histo.most_common(1)[0][0]
self.feature.vector[0] = pNumberMax
except IndexError:
self.feature.vector[0] = 0.0
[docs]
class PrimaryRegisterFeature(featuresModule.FeatureExtractor):
'''
Average MIDI pitch.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.PrimaryRegisterFeature(s)
>>> fe.extract().vector
[61.12...]
'''
id = 'P12'
name = 'Primary Register'
description = 'Average MIDI pitch.'
discrete = False
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches']
if not histo:
raise JSymbolicFeatureException('input lacks notes')
self.feature.vector[0] = statistics.mean([p.ps for p in histo])
[docs]
class ImportanceOfBassRegisterFeature(featuresModule.FeatureExtractor):
'''
Fraction of Notes between MIDI pitches 0 and 54.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.ImportanceOfBassRegisterFeature(s)
>>> fe.extract().vector
[0.184...]
'''
id = 'P13'
name = 'Importance of Bass Register'
description = 'Fraction of Note Ons between MIDI pitches 0 and 54.'
discrete = False
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
if not histo:
raise JSymbolicFeatureException('input lacks notes')
matches = []
# assuming we just average the active pitch values
for i, count in histo.items():
if i <= 54: # index is midi note number
matches.append(count)
matchedSum = sum(matches)
# divide number found by total
self.feature.vector[0] = matchedSum / sum(histo.values())
[docs]
class ImportanceOfMiddleRegisterFeature(featuresModule.FeatureExtractor):
'''
Fraction of Notes between MIDI pitches 55 and 72.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.ImportanceOfMiddleRegisterFeature(s)
>>> fe.extract().vector
[0.766...]
'''
id = 'P14'
name = 'Importance of Middle Register'
description = 'Fraction of Note Ons between MIDI pitches 55 and 72.'
discrete = False
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
if not histo:
raise JSymbolicFeatureException('input lacks notes')
matches = []
# assuming we just average the active pitch values
for i, count in histo.items():
if 55 <= i <= 72: # index is midi note number
matches.append(count)
matchedSum = sum(matches)
# divide number found by total
self.feature.vector[0] = matchedSum / sum(histo.values())
[docs]
class ImportanceOfHighRegisterFeature(featuresModule.FeatureExtractor):
'''
Fraction of Notes between MIDI pitches 73 and 127.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.ImportanceOfHighRegisterFeature(s)
>>> fe.extract().vector
[0.049...]
'''
id = 'P15'
name = 'Importance of High Register'
description = 'Fraction of Note Ons between MIDI pitches 73 and 127.'
discrete = False
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.midiPitchHistogram']
if not histo:
raise JSymbolicFeatureException('input lacks notes')
matches = []
# assuming we just average the active pitch values
for i, count in histo.items():
if i >= 73: # index is midi note number
matches.append(count)
matchedSum = sum(matches)
# divide number found by total
self.feature.vector[0] = matchedSum / sum(histo.values())
[docs]
class MostCommonPitchClassFeature(featuresModule.FeatureExtractor):
'''
Bin label of the most common pitch class.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MostCommonPitchClassFeature(s)
>>> fe.extract().vector
[1]
'''
id = 'P16'
name = 'Most Common Pitch Class'
description = 'Bin label of the most common pitch class.'
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
histo = self.data['pitches.pitchClassHistogram']
pIndexMax = histo.index(max(histo))
self.feature.vector[0] = pIndexMax
[docs]
class DominantSpreadFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
Largest number of consecutive pitch classes separated by perfect
5ths that accounted for at least 9% each of the notes.
'''
id = 'P17'
name = 'Dominant Spread'
description = ('Largest number of consecutive pitch classes separated by '
'perfect 5ths that accounted for at least 9% each of the notes.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class StrongTonalCentresFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
Number of peaks in the fifths pitch histogram that each account
for at least 9% of all Note Ons.
'''
id = 'P18'
name = 'Strong Tonal Centres'
description = ('Number of peaks in the fifths pitch histogram that each account '
'for at least 9% of all Note Ons.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class BasicPitchHistogramFeature(featuresModule.FeatureExtractor):
'''
A feature extractor that finds a features array with bins corresponding
to the values of the basic pitch histogram.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.BasicPitchHistogramFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.006..., 0.0, 0.0, 0.006..., 0.006..., 0.030...,
0.0, 0.036..., 0.012..., 0.0, 0.006..., 0.018..., 0.061..., 0.0,
0.042..., 0.073..., 0.012..., 0.092..., 0.0, 0.116..., 0.061...,
0.006..., 0.085..., 0.018..., 0.110..., 0.0, 0.042..., 0.055...,
0.0, 0.049..., 0.0, 0.042..., 0.0, 0.0, 0.006..., 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
'''
id = 'P19'
name = 'Basic Pitch Histogram'
description = ('A features array with bins corresponding to the '
'values of the basic pitch histogram.')
dimensions = 128
normalize = True
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
for i, count in self.data['pitches.midiPitchHistogram'].items():
self.feature.vector[i] = count
[docs]
class PitchClassDistributionFeature(featuresModule.FeatureExtractor):
'''
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.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.PitchClassDistributionFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.196..., 0.073..., 0.006..., 0.098..., 0.036..., 0.177..., 0.0,
0.085..., 0.134..., 0.018..., 0.171..., 0.0]
'''
id = 'P20'
name = 'Pitch Class Distribution'
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.')
dimensions = 12
discrete = False
normalize = True
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# Create vector with [C, C#, D, etc.]
temp = [0] * self.dimensions
for i, count in enumerate(self.data['pitches.pitchClassHistogram']):
temp[i] = count
# Now rearrange so that the most common is in array 0 as per
# original jSymbolic documentation and implementation
m = temp.index(max(temp))
# m will become 0, m + 1 will become 1, etc.
for i, val in enumerate(temp):
self.feature.vector[(i - m) % self.dimensions] = val
[docs]
class FifthsPitchHistogramFeature(featuresModule.FeatureExtractor):
'''
A feature array with bins corresponding to the values of the 5ths pitch class
histogram. Instead of the bins being arranged according to semitones --
[C, C#, D, etc.] -- they are arranged according to the circle of fifths:
[C, G, D, A, E, B, F#, C#, G#, D#, A#, F]. Viewing such a histogram
may draw attention to the prevalence of a tonal center, including the
prevalence of dominant relationships in the piece.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.FifthsPitchHistogramFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.0, 0.0, 0.073..., 0.134..., 0.098..., 0.171..., 0.177..., 0.196...,
0.085..., 0.006..., 0.018..., 0.036...]
'''
id = 'P21'
name = 'Fifths Pitch Histogram'
description = ('A feature array with bins corresponding to the values of the '
'5ths pitch class histogram.')
dimensions = 12
normalize = True
# pitch-class to circle-of-fifths index mapping
_mapping = {i: (7 * i) % 12 for i in range(12)}
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
for i, count in enumerate(self.data['pitches.pitchClassHistogram']):
self.feature.vector[self._mapping[i]] = count
[docs]
class QualityFeature(featuresModule.FeatureExtractor):
'''
Set to 0 if the key signature indicates that
a recording is major, set to 1 if it indicates
that it is minor. In jSymbolic, this is set to 0 if key signature is unknown.
See features.native.QualityFeature for a music21 improvement on this method
Example: Handel, Rinaldo Aria (musicxml) is explicitly encoded as being in Major:
>>> s = corpus.parse('handel/rinaldo/lascia_chio_pianga')
>>> fe = features.jSymbolic.QualityFeature(s)
>>> f = fe.extract()
>>> f.vector
[0]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.QualityFeature(s)
>>> f = fe.extract()
>>> f.vector
[1]
'''
id = 'P22'
name = 'Quality'
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.
'''
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
allKeys = self.data['flat.getElementsByClass(Key)']
keyFeature = None
for x in allKeys:
if x.mode == 'major':
keyFeature = 0
break
elif x.mode == 'minor':
keyFeature = 1
break
if keyFeature is None:
keyFeature = 0
self.feature.vector[0] = keyFeature
[docs]
class GlissandoPrevalenceFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented in music21.
Number of Note Ons that have at least one MIDI Pitch Bend associated
with them divided by total number of pitched Note Ons.
'''
id = 'P23'
name = 'Glissando Prevalence'
description = ('Number of Note Ons that have at least one MIDI Pitch Bend '
'associated with them divided by total number of pitched Note Ons.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class AverageRangeOfGlissandosFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented in music21.
Average range of MIDI Pitch Bends, where "range" is defined
as the greatest value of the absolute difference between 64 and the
second data byte of all MIDI Pitch Bend messages falling between the
Note On and Note Off messages of any note
'''
id = 'P24'
name = 'Average Range Of Glissandos'
description = ('Average range of MIDI Pitch Bends, where "range" is '
'defined as the greatest value of the absolute difference '
'between 64 and the second data byte of all MIDI Pitch Bend '
'messages falling between the Note On and Note Off messages '
'of any note.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class VibratoPrevalenceFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented in music21.
Number of notes for which Pitch Bend messages change direction at least twice divided by
total number of notes that have Pitch Bend messages associated with them.
'''
id = 'P25'
name = 'Vibrato Prevalence'
description = ('Number of notes for which Pitch Bend messages change '
'direction at least twice divided by total number of notes '
'that have Pitch Bend messages associated with them.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class PrevalenceOfMicrotonesFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented.
Number of Note Ons that are preceded by isolated MIDI Pitch Bend
messages as a fraction of the total number of Note Ons.'
'''
id = 'P26'
name = 'Prevalence Of Microtones'
description = ('Number of Note Ons that are preceded by isolated MIDI Pitch '
'Bend messages as a fraction of the total number of Note Ons.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
# ------------------------------------------------------------------------------
# rhythm
[docs]
class StrongestRhythmicPulseFeature(featuresModule.FeatureExtractor):
'''
Bin label of the beat bin of the peak with the highest frequency.
>>> sch = corpus.parse('schoenberg/opus19', 2)
>>> for p in sch.parts:
... p.insert(0, tempo.MetronomeMark('Langsam', 70))
>>> fe = features.jSymbolic.StrongestRhythmicPulseFeature(sch)
>>> f = fe.extract()
>>> f.vector[0]
140
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.StrongestRhythmicPulseFeature(s)
>>> f = fe.extract()
>>> f.vector
[96]
'''
id = 'R1'
name = 'Strongest Rhythmic Pulse'
description = 'Bin label of the beat bin with the highest frequency.'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
beatHisto = self.data['flat.secondsMap.beatHistogram']
self.feature.vector[0] = beatHisto.index(max(beatHisto))
[docs]
class SecondStrongestRhythmicPulseFeature(featuresModule.FeatureExtractor):
'''
Bin label of the beat bin of the peak with the second-highest frequency.
>>> sch = corpus.parse('schoenberg/opus19', 2)
>>> for p in sch.parts:
... p.insert(0, tempo.MetronomeMark('Langsam', 70))
>>> fe = features.jSymbolic.SecondStrongestRhythmicPulseFeature(sch)
>>> f = fe.extract()
>>> f.vector[0]
70
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.SecondStrongestRhythmicPulseFeature(s)
>>> f = fe.extract()
>>> f.vector
[192]
'''
id = 'R2'
name = 'Second Strongest Rhythmic Pulse'
description = ('Bin label of the beat bin of the peak '
'with the second highest frequency.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram'])
highestIndex = beatHisto.index(max(beatHisto))
beatHisto[highestIndex] = 0
self.feature.vector[0] = beatHisto.index(max(beatHisto))
[docs]
class HarmonicityOfTwoStrongestRhythmicPulsesFeature(
featuresModule.FeatureExtractor):
'''
The bin label of the higher (in terms of bin label) of the two beat bins of the
peaks with the highest frequency divided by the bin label of the lower.
>>> sch = corpus.parse('schoenberg/opus19', 2)
>>> for p in sch.parts:
... p.insert(0, tempo.MetronomeMark('Langsam', 70))
>>> fe = features.jSymbolic.HarmonicityOfTwoStrongestRhythmicPulsesFeature(sch)
>>> f = fe.extract()
>>> f.vector[0]
2.0
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.HarmonicityOfTwoStrongestRhythmicPulsesFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.5]
'''
id = 'R3'
name = 'Harmonicity of Two Strongest Rhythmic Pulses'
description = ('The bin label of the higher (in terms of bin label) of the '
'two beat bins of the peaks with the highest frequency '
'divided by the bin label of the lower.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram'])
highestIndex = beatHisto.index(max(beatHisto))
beatHisto[highestIndex] = 0
secondHighest = beatHisto.index(max(beatHisto))
self.feature.vector[0] = float(highestIndex / secondHighest)
[docs]
class StrengthOfStrongestRhythmicPulseFeature(featuresModule.FeatureExtractor):
'''
Frequency of the beat bin with the highest frequency.
>>> sch = corpus.parse('schoenberg/opus19', 2)
>>> for p in sch.parts:
... p.insert(0, tempo.MetronomeMark('Langsam', 70))
>>> fe = features.jSymbolic.StrengthOfStrongestRhythmicPulseFeature(sch)
>>> fe.extract().vector[0]
0.853...
'''
id = 'R4'
name = 'Strength of Strongest Rhythmic Pulse'
description = 'Frequency of the beat bin with the highest frequency.'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
beatHisto = self.data['flat.secondsMap.beatHistogram']
self.feature.vector[0] = max(beatHisto) / sum(beatHisto)
[docs]
class StrengthOfSecondStrongestRhythmicPulseFeature(
featuresModule.FeatureExtractor):
'''
Frequency of the beat bin of the peak with the second-highest frequency.
>>> sch = corpus.parse('schoenberg/opus19', 2)
>>> for p in sch.parts:
... p.insert(0, tempo.MetronomeMark('Langsam', 70))
>>> fe = features.jSymbolic.StrengthOfSecondStrongestRhythmicPulseFeature(sch)
>>> fe.extract().vector[0]
0.121...
'''
id = 'R5'
name = 'Strength of Second Strongest Rhythmic Pulse'
description = ('Frequency of the beat bin of the peak '
'with the second highest frequency.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram'])
sumHisto = sum(beatHisto)
highestIndex = beatHisto.index(max(beatHisto))
beatHisto[highestIndex] = 0
secondHighest = max(beatHisto)
self.feature.vector[0] = secondHighest / sumHisto
[docs]
class StrengthRatioOfTwoStrongestRhythmicPulsesFeature(
featuresModule.FeatureExtractor):
'''
The frequency of the higher (in terms of frequency) of the two beat bins
corresponding to the peaks with the highest frequency divided by the frequency of the lower.
>>> sch = corpus.parse('schoenberg/opus19', 2)
>>> for p in sch.parts:
... p.insert(0, tempo.MetronomeMark('Langsam', 70))
>>> fe = features.jSymbolic.StrengthRatioOfTwoStrongestRhythmicPulsesFeature(sch)
>>> fe.extract().vector[0]
7.0
'''
id = 'R6'
name = 'Strength Ratio of Two Strongest Rhythmic Pulses'
description = ('The frequency of the higher (in terms of frequency) of the two '
'beat bins corresponding to the peaks with the highest '
'frequency divided by the frequency of the lower.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram'])
theHighest = max(beatHisto)
highestIndex = beatHisto.index(theHighest)
beatHisto[highestIndex] = 0
secondHighest = max(beatHisto)
self.feature.vector[0] = theHighest / secondHighest
[docs]
class CombinedStrengthOfTwoStrongestRhythmicPulsesFeature(
featuresModule.FeatureExtractor):
'''
The sum of the frequencies of the two beat bins of the peaks with the highest frequencies.
>>> sch = corpus.parse('schoenberg/opus19', 2)
>>> for p in sch.parts:
... p.insert(0, tempo.MetronomeMark('Langsam', 70))
>>> fe = features.jSymbolic.CombinedStrengthOfTwoStrongestRhythmicPulsesFeature(sch)
>>> fe.extract().vector[0]
0.975...
'''
id = 'R7'
name = 'Combined Strength of Two Strongest Rhythmic Pulses'
description = ('The sum of the frequencies of the two beat bins '
'of the peaks with the highest frequencies.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
beatHisto = copy.copy(self.data['flat.secondsMap.beatHistogram'])
sumHisto = sum(beatHisto)
theHighest = max(beatHisto)
highestIndex = beatHisto.index(theHighest)
beatHisto[highestIndex] = 0
secondHighest = max(beatHisto)
self.feature.vector[0] = (theHighest + secondHighest) / sumHisto
[docs]
class NumberOfStrongPulsesFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented.
Number of beat peaks with normalized frequencies over 0.1.
'''
id = 'R8'
name = 'Number of Strong Pulses'
description = 'Number of beat peaks with normalized frequencies over 0.1.'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class NumberOfModeratePulsesFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented.
Number of beat peaks with normalized frequencies over 0.01.
'''
id = 'R9'
name = 'Number of Moderate Pulses'
description = 'Number of beat peaks with normalized frequencies over 0.01.'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class NumberOfRelativelyStrongPulsesFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented.
Number of beat peaks with frequencies at least 30% as high as the
frequency of the bin with the highest frequency.
'''
id = 'R10'
name = 'Number of Relatively Strong Pulses'
description = ('Number of beat peaks with frequencies at least 30% as high as '
'the frequency of the bin with the highest frequency.')
[docs]
class RhythmicLoosenessFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented.
Average width of beat histogram peaks (in beats per minute).
Width is measured for all peaks with frequencies at least 30% as high as the highest peak,
and is defined by the distance between the points on the peak in question that are
30% of the height of the peak.
'''
id = 'R11'
name = 'Rhythmic Looseness'
description = dedent('''
Average width of beat histogram peaks (in beats per minute).
Width is measured for all peaks with frequencies at least 30% as high as the
highest peak, and is defined by the distance between the points on the peak in
question that are 30% of the height of the peak.''')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class PolyrhythmsFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented.
Number of beat peaks with frequencies at least 30% of the highest frequency
whose bin labels are not integer multiples or factors
(using only multipliers of 1, 2, 3, 4, 6 and 8) (with an accepted
error of +/- 3 bins) of the bin label of the peak with the highest frequency.
This number is then divided by the total number of beat bins with frequencies
over 30% of the highest frequency.
'''
id = 'R12'
name = 'Polyrhythms'
description = '''
Number of beat peaks with frequencies at least 30% of the highest frequency
whose bin labels are not integer multiples or factors
(using only multipliers of 1, 2, 3, 4, 6 and 8) (with an accepted
error of +/- 3 bins) of the bin label of the peak with the highest frequency.
This number is then divided by the total number of beat bins with frequencies
over 30% of the highest frequency.'''
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class RhythmicVariabilityFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented.
Standard deviation of the bin values (except the first 40 empty ones).
'''
id = 'R13'
name = 'Rhythmic Variability'
description = 'Standard deviation of the bin values (except the first 40 empty ones).'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class BeatHistogramFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented.
A feature extractor that finds a feature array with entries corresponding to the frequency
values of each of the bins of the beat histogram (except the first 40 empty ones).
'''
id = 'R14'
name = 'Beat Histogram'
description = ('A feature array with entries corresponding to the '
'frequency values of each of the bins of the beat histogram '
'(except the first 40 empty ones).')
dimensions = 161
discrete = False
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class NoteDensityFeature(featuresModule.FeatureExtractor):
'''
Gives the average number of notes per second, taking into account
the tempo at any moment in the piece. Unlike jSymbolic, music21
quantizes notes from MIDI somewhat before running this test; this
function is meant to be run on encoded MIDI scores rather than
recorded MIDI performances.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.NoteDensityFeature(s)
>>> f = fe.extract()
>>> f.vector
[7.244...]
'''
id = 'R15'
name = 'Note Density'
description = 'Average number of notes per second.'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
secondsMap = self.data['flat.secondsMap']
# The average number of notes per second in the piece is calculated
# by taking the total number of notes in the piece and dividing by
# the end time of the piece (in seconds).
end_times = [bundle['endTimeSeconds'] for bundle in secondsMap]
end_times.sort() # may already be sorted?
# Create a list of difference in time offset between consecutive notes
if not end_times:
self.feature.vector[0] = 0.0
else:
self.feature.vector[0] = float(len(end_times)) / end_times[-1]
[docs]
class AverageNoteDurationFeature(featuresModule.FeatureExtractor):
'''
Average duration of notes in seconds.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.AverageNoteDurationFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.552...]
>>> s.insert(0, tempo.MetronomeMark(number=240))
>>> fe = features.jSymbolic.AverageNoteDurationFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.220858...]
'''
id = 'R17'
name = 'Average Note Duration'
description = 'Average duration of notes in seconds.'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
secondsMap = self.data['flat.secondsMap']
if not secondsMap:
raise JSymbolicFeatureException('input lacks notes')
total = 0.0
for bundle in secondsMap:
total += bundle['durationSeconds']
self.feature.vector[0] = total / len(secondsMap)
[docs]
class VariabilityOfNoteDurationFeature(featuresModule.FeatureExtractor):
'''
Standard deviation of note durations in seconds.
# In this piece, we have:
# 9 half notes or tied pair of quarters
# 98 untied quarters or tied pair of eighths
# 56 untied eighths
# BPM = 120 means a half note is a second.
# Mean duration should thus be 0.44171779141104295
# and standard deviation should be 0.17854763448902145
>>> s = corpus.parse('bwv66.6')
>>> for p in s.parts:
... p.insert(0, tempo.MetronomeMark(number=120))
>>> fe = features.jSymbolic.VariabilityOfNoteDurationFeature(s)
>>> f = fe.extract()
>>> f.vector[0]
0.178...
'''
id = 'R18'
name = 'Variability of Note Duration'
description = 'Standard deviation of note durations in seconds.'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
secondsMap = self.data['flat.secondsMap']
if not secondsMap:
raise JSymbolicFeatureException('input lacks notes')
note_durations = []
for bundle in secondsMap:
note_durations.append(bundle['durationSeconds'])
self.feature.vector[0] = statistics.pstdev(note_durations)
[docs]
class MaximumNoteDurationFeature(featuresModule.FeatureExtractor):
'''
Duration of the longest note (in seconds).
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MaximumNoteDurationFeature(s)
>>> f = fe.extract()
>>> f.vector
[1.25]
'''
id = 'R19'
name = 'Maximum Note Duration'
description = 'Duration of the longest note (in seconds).'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
secondsMap = self.data['flat.secondsMap']
if not secondsMap:
raise JSymbolicFeatureException('input lacks notes')
maxSeconds = 0.0
for bundle in secondsMap:
if bundle['durationSeconds'] > maxSeconds:
maxSeconds = bundle['durationSeconds']
self.feature.vector[0] = maxSeconds
[docs]
class MinimumNoteDurationFeature(featuresModule.FeatureExtractor):
'''
Duration of the shortest note (in seconds).
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MinimumNoteDurationFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.3125]
'''
id = 'R20'
name = 'Minimum Note Duration'
description = 'Duration of the shortest note (in seconds).'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
secondsMap = self.data['flat.secondsMap']
if not secondsMap:
raise JSymbolicFeatureException('input lacks notes')
# an arbitrary number from the coll
minSeconds = secondsMap[0]['durationSeconds']
for bundle in secondsMap:
if bundle['durationSeconds'] < minSeconds:
minSeconds = bundle['durationSeconds']
self.feature.vector[0] = minSeconds
[docs]
class StaccatoIncidenceFeature(featuresModule.FeatureExtractor):
'''
Number of notes with durations of less than a 10th of a second divided by
the total number of notes in the recording.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.StaccatoIncidenceFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.0]
'''
id = 'R21'
name = 'Staccato Incidence'
description = ('Number of notes with durations of less than a 10th '
'of a second divided by the total number of notes in the recording.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
secondsMap = self.data['flat.secondsMap']
if not secondsMap:
raise JSymbolicFeatureException('input lacks notes')
count = 0
for bundle in secondsMap:
if bundle['durationSeconds'] < 0.10:
count += 1
self.feature.vector[0] = count / len(secondsMap)
[docs]
class AverageTimeBetweenAttacksFeature(featuresModule.FeatureExtractor):
'''
Average time in seconds between Note On events (regardless of channel).
>>> s = corpus.parse('bwv66.6')
>>> for p in s.parts:
... p.insert(0, tempo.MetronomeMark(number=120))
>>> fe = features.jSymbolic.AverageTimeBetweenAttacksFeature(s)
>>> f = fe.extract()
>>> print(f.vector)
[0.35]
'''
id = 'R22'
name = 'Average Time Between Attacks'
description = 'Average time in seconds between Note On events (regardless of channel).'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
secondsMap = self.data['flat.secondsMap']
# Get a list of note onset times
onsets = [bundle['offsetSeconds'] for bundle in secondsMap]
if not onsets:
raise JSymbolicFeatureException('input lacks notes')
onsets.sort() # may already be sorted?
# Create a list of difference in time offset between consecutive notes
differences = []
for i, o in enumerate(onsets):
if i == len(onsets) - 1: # stop before the last
break
oNext = onsets[i + 1]
# Don't include simultaneous attacks
dif = oNext - o
if not isclose(dif, 0.0, abs_tol=1e-7):
differences.append(dif)
self.feature.vector[0] = sum(differences) / len(differences)
[docs]
class VariabilityOfTimeBetweenAttacksFeature(featuresModule.FeatureExtractor):
'''
Standard deviation of the times, in seconds, between Note On events (regardless of channel).
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.VariabilityOfTimeBetweenAttacksFeature(s)
>>> f = fe.extract()
>>> print(f.vector)
[0.1875]
'''
id = 'R23'
name = 'Variability of Time Between Attacks'
description = ('Standard deviation of the times, in seconds, '
'between Note On events (regardless of channel).')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
secondsMap = self.data['flat.secondsMap']
# Create a list of difference in time offset between consecutive notes
onsets = [bundle['offsetSeconds'] for bundle in secondsMap]
if not onsets:
raise JSymbolicFeatureException('input lacks notes')
onsets.sort() # may already be sorted?
differences = []
for i, o in enumerate(onsets):
if i == len(onsets) - 1: # stop before the last
break
oNext = onsets[i + 1]
# Don't include simultaneous attacks
dif = oNext - o
if not isclose(dif, 0.0, abs_tol=1e-7):
differences.append(dif)
self.feature.vector[0] = statistics.pstdev(differences)
[docs]
class AverageTimeBetweenAttacksForEachVoiceFeature(
featuresModule.FeatureExtractor):
'''
Average of average times in seconds between Note On events on individual channels
that contain at least one note.
>>> s = corpus.parse('bwv66.6')
>>> for p in s.parts:
... p.insert(0, tempo.MetronomeMark(number=120))
>>> fe = features.jSymbolic.AverageTimeBetweenAttacksForEachVoiceFeature(s)
>>> f = fe.extract()
>>> print(f.vector[0])
0.442...
'''
id = 'R24'
name = 'Average Time Between Attacks For Each Voice'
description = ('Average of average times in seconds between Note On events '
'on individual channels that contain at least one note.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
onsetsByPart = []
avgByPart = []
if self.data.partsCount > 0:
for i in range(self.data.partsCount):
secondsMap = self.data['parts'][i]['flat.secondsMap']
onsets = [bundle['offsetSeconds'] for bundle in secondsMap]
onsetsByPart.append(onsets)
else:
secondsMap = self.data['flat.secondsMap']
onsets = [bundle['offsetSeconds'] for bundle in secondsMap]
onsetsByPart.append(onsets)
for onsets in onsetsByPart:
# Create a list of difference in time offset between consecutive notes
onsets.sort() # may already be sorted?
differences = []
for i, o in enumerate(onsets):
if i == len(onsets) - 1: # stop before the last
break
oNext = onsets[i + 1]
# Don't include simultaneous attacks
dif = oNext - o
if not isclose(dif, 0.0, abs_tol=1e-7):
differences.append(dif)
if not differences:
raise JSymbolicFeatureException('at least one part lacks notes')
avgByPart.append(sum(differences) / len(differences))
self.feature.vector[0] = sum(avgByPart) / len(avgByPart)
[docs]
class AverageVariabilityOfTimeBetweenAttacksForEachVoiceFeature(
featuresModule.FeatureExtractor):
'''
Average standard deviation, in seconds, of time between Note On events on individual
channels that contain at least one note.
>>> s = corpus.parse('bwv66.6')
>>> for p in s.parts:
... p.insert(0, tempo.MetronomeMark(number=120))
>>> fe = features.jSymbolic.AverageVariabilityOfTimeBetweenAttacksForEachVoiceFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.177...]
'''
id = 'R25'
name = 'Average Variability of Time Between Attacks For Each Voice'
description = ('Average standard deviation, in seconds, of time between '
'Note On events on individual channels that contain '
'at least one note.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
onsetsByPart = []
stdDeviationByPart = []
if self.data.partsCount > 0:
for i in range(self.data.partsCount):
secondsMap = self.data['parts'][i]['flat.secondsMap']
onsets = [bundle['offsetSeconds'] for bundle in secondsMap]
onsetsByPart.append(onsets)
else:
secondsMap = self.data['flat.secondsMap']
onsets = [bundle['offsetSeconds'] for bundle in secondsMap]
onsetsByPart.append(onsets)
for onsets in onsetsByPart:
# Create a list of difference in time offset between consecutive notes
onsets.sort() # may already be sorted?
differences = []
for i, o in enumerate(onsets):
if i == len(onsets) - 1: # stop before the last
break
oNext = onsets[i + 1]
dif = oNext - o
# Don't include simultaneous attacks
if not isclose(dif, 0.0, abs_tol=1e-7):
differences.append(dif)
if not differences:
raise JSymbolicFeatureException('at least one part lacks notes')
stdDeviationByPart.append(statistics.pstdev(differences))
self.feature.vector[0] = (sum(stdDeviationByPart)
/ len(stdDeviationByPart))
# class IncidenceOfCompleteRestsFeature(featuresModule.FeatureExtractor):
# '''
# Not implemented in jSymbolic
#
# '''
# def __init__(self, dataOrStream=None, **keywords):
# super().__init__(dataOrStream=dataOrStream,
# **keywords)
#
# self.name = 'Incidence Of Complete Rests'
# self.description = ('Total amount of time in seconds in which no notes are sounding'
# ' on any channel divided by the total length of the recording')
# self.isSequential = True
# self.dimensions = 1
#
# class MaximumCompleteRestDurationFeature(featuresModule.FeatureExtractor):
# '''
# Not implemented in jSymbolic
#
# '''
# def __init__(self, dataOrStream=None, **keywords):
# super().__init__(dataOrStream=dataOrStream,
# **keywords)
#
# self.name = 'Maximum Complete Rest Duration'
# self.description = ('Maximum amount of time in seconds in which no notes '
# 'are sounding on any channel.')
# self.isSequential = True
# self.dimensions = 1
#
# class AverageRestDurationPerVoiceFeature(featuresModule.FeatureExtractor):
# '''
# Not implemented in jSymbolic
#
# '''
# def __init__(self, dataOrStream=None, **keywords):
# super().__init__(dataOrStream=dataOrStream,
# **keywords)
#
# self.name = 'Average Rest Duration Per Voice'
# self.description = ('Average, in seconds, of the average amounts of time in each '
# 'channel in which no note is sounding (counting only channels with at least '
# 'one note), divided by the total duration of the recording')
# self.isSequential = True
# self.dimensions = 1
#
# class AverageVariabilityOfRestDurationsAcrossVoicesFeature(featuresModule.FeatureExtractor):
# '''
# Not implemented in jSymbolic
#
# '''
# def __init__(self, dataOrStream=None, **keywords):
# super().__init__(dataOrStream=dataOrStream,
# **keywords)
#
# self.name = 'Average Variability Of Rest Durations Across Voices'
# self.description = ('Standard deviation, in seconds, of the average amounts of time '
# 'in each channel in which no note is sounding (counting only '
# 'channels with at least one note)'
# self.isSequential = True
# self.dimensions = 1
[docs]
class InitialTempoFeature(featuresModule.FeatureExtractor):
'''
Tempo in beats per minute at the start of the recording.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.InitialTempoFeature(s)
>>> f = fe.extract()
>>> f.vector # a default
[96.0]
'''
id = 'R30'
name = 'Initial Tempo'
description = 'Tempo in beats per minute at the start of the recording.'
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
triples = self.data['metronomeMarkBoundaries']
# the first is the default, if necessary; also provides start/end time
mm = triples[0][2]
# assume we want quarter bpm, not bpm in other division
self.feature.vector[0] = mm.getQuarterBPM()
[docs]
class InitialTimeSignatureFeature(featuresModule.FeatureExtractor):
'''
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.
>>> s1 = stream.Stream()
>>> s1.append(meter.TimeSignature('3/4'))
>>> fe = features.jSymbolic.InitialTimeSignatureFeature(s1)
>>> fe.extract().vector
[3, 4]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.InitialTimeSignatureFeature(s)
>>> f = fe.extract()
>>> f.vector
[4, 4]
'''
id = 'R31'
name = 'Initial Time Signature'
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.')
dimensions = 2
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
elements = self.data['flat.getElementsByClass(TimeSignature)']
if not elements:
return # vector already zero
ts = elements[0]
environLocal.printDebug(['found ts', ts])
self.feature.vector[0] = elements[0].numerator
self.feature.vector[1] = elements[0].denominator
[docs]
class CompoundOrSimpleMeterFeature(featuresModule.FeatureExtractor):
'''
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).
>>> s1 = stream.Stream()
>>> s1.append(meter.TimeSignature('3/4'))
>>> fe = features.jSymbolic.CompoundOrSimpleMeterFeature(s1)
>>> fe.extract().vector
[0]
>>> s2 = stream.Stream()
>>> s2.append(meter.TimeSignature('9/8'))
>>> fe.setData(s2) # change the data
>>> fe.extract().vector
[1]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.CompoundOrSimpleMeterFeature(s)
>>> f = fe.extract()
>>> f.vector
[0]
'''
id = 'R32'
name = 'Compound Or Simple Meter'
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).')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
elements = self.data['flat.getElementsByClass(TimeSignature)']
if elements:
try:
countName = elements[0].beatDivisionCountName
except exceptions21.TimeSignatureException:
return # do nothing
if countName == 'Compound':
self.feature.vector[0] = 1
[docs]
class TripleMeterFeature(featuresModule.FeatureExtractor):
'''
Set to 1 if numerator of initial time signature is 3, set to 0 otherwise.
>>> s1 = stream.Stream()
>>> s1.append(meter.TimeSignature('5/4'))
>>> fe = features.jSymbolic.TripleMeterFeature(s1)
>>> fe.extract().vector
[0]
>>> s2 = stream.Stream()
>>> s2.append(meter.TimeSignature('3/4'))
>>> fe.setData(s2) # change the data
>>> fe.extract().vector
[1]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.TripleMeterFeature(s)
>>> f = fe.extract()
>>> f.vector
[0]
'''
id = 'R33'
name = 'Triple Meter'
description = ('Set to 1 if numerator of initial time signature is 3, '
'set to 0 otherwise.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
elements = self.data['flat.getElementsByClass(TimeSignature)']
# not: not looking at other triple meters
if elements and elements[0].numerator == 3:
self.feature.vector[0] = 1
[docs]
class QuintupleMeterFeature(featuresModule.FeatureExtractor):
'''
Set to 1 if numerator of initial time signature is 5, set to 0 otherwise.
>>> s1 = stream.Stream()
>>> s1.append(meter.TimeSignature('5/4'))
>>> fe = features.jSymbolic.QuintupleMeterFeature(s1)
>>> fe.extract().vector
[1]
>>> s2 = stream.Stream()
>>> s2.append(meter.TimeSignature('3/4'))
>>> fe.setData(s2) # change the data
>>> fe.extract().vector
[0]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.QuintupleMeterFeature(s)
>>> f = fe.extract()
>>> f.vector
[0]
'''
id = 'R34'
name = 'Quintuple Meter'
description = ('Set to 1 if numerator of initial time signature is 5, '
'set to 0 otherwise.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
elements = self.data['flat.getElementsByClass(TimeSignature)']
if elements and elements[0].numerator == 5:
self.feature.vector[0] = 1
[docs]
class ChangesOfMeterFeature(featuresModule.FeatureExtractor):
'''
Returns 1 if the time signature is changed one or more
times during the recording.
>>> s1 = stream.Stream()
>>> s1.append(meter.TimeSignature('3/4'))
>>> fe = features.jSymbolic.ChangesOfMeterFeature(s1)
>>> fe.extract().vector
[0]
>>> s2 = stream.Stream()
>>> s2.append(meter.TimeSignature('3/4'))
>>> s2.append(meter.TimeSignature('4/4'))
>>> fe.setData(s2) # change the data
>>> fe.extract().vector
[1]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.ChangesOfMeterFeature(s)
>>> f = fe.extract()
>>> f.vector
[0]
'''
id = 'R35'
name = 'Changes of Meter'
description = ('Set to 1 if the time signature is changed one or more '
'times during the recording.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
elements = self.data['flat.getElementsByClass(TimeSignature)']
if len(elements) <= 1:
return # vector already zero
first = elements[0]
for e in elements[1:]:
if not first.ratioEqual(e):
self.feature.vector[0] = 1
return
[docs]
class DurationFeature(featuresModule.FeatureExtractor):
'''
A feature extractor that extracts the duration of the piece in seconds.
>>> s = corpus.parse('bwv66.6')
>>> for p in s.parts:
... p.insert(0, tempo.MetronomeMark(number=120))
>>> fe = features.jSymbolic.DurationFeature(s)
>>> f = fe.extract()
>>> f.vector[0]
18.0
'''
id = 'R36'
name = 'Duration'
description = 'The total duration in seconds of the music.'
isSequential = False # this is the only jSymbolic non seq feature
discrete = False
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
secondsMap = self.data['flat.secondsMap']
if not secondsMap:
raise JSymbolicFeatureException('input lacks duration')
# The total duration of the piece is the same as the latest end time
# of all the notes.
end_times = [bundle['endTimeSeconds'] for bundle in secondsMap]
end_times.sort() # may already be sorted?
self.feature.vector[0] = end_times[-1]
# ------------------------------------------------------------------------------
# dynamics
[docs]
class OverallDynamicRangeFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
The maximum loudness minus the minimum loudness value.
TODO: implement
'''
id = 'D1'
name = 'Overall Dynamic Range'
description = 'The maximum loudness minus the minimum loudness value.'
[docs]
class VariationOfDynamicsFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
Standard deviation of loudness levels of all notes.
TODO: implement
'''
id = 'D2'
name = 'Variation of Dynamics'
description = 'Standard deviation of loudness levels of all notes.'
[docs]
class VariationOfDynamicsInEachVoiceFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
The average of the standard deviations of loudness levels within each
channel that contains at least one note.
TODO: implement
'''
id = 'D3'
name = 'Variation of Dynamics In Each Voice'
description = ('The average of the standard deviations of loudness '
'levels within each channel that contains at least one note.')
[docs]
class AverageNoteToNoteDynamicsChangeFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
Average change of loudness from one note to the next note in the
same channel (in MIDI velocity units).
TODO: implement
'''
id = 'D4'
name = 'Average Note To Note Dynamics Change'
description = ('Average change of loudness from one note to the next note '
'in the same channel (in MIDI velocity units).')
# ------------------------------------------------------------------------------
# texture based
[docs]
class MaximumNumberOfIndependentVoicesFeature(featuresModule.FeatureExtractor):
'''
Maximum number of different channels in which notes have sounded simultaneously.
Here, Parts are treated as channels.
>>> s = corpus.parse('handel/rinaldo/lascia_chio_pianga')
>>> fe = features.jSymbolic.MaximumNumberOfIndependentVoicesFeature(s)
>>> f = fe.extract()
>>> f.vector
[3]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.MaximumNumberOfIndependentVoicesFeature(s)
>>> f = fe.extract()
>>> f.vector
[4]
'''
id = 'T1'
name = 'Maximum Number of Independent Voices'
description = ('Maximum number of different channels in which notes '
'have sounded simultaneously. Here, Parts are treated as channels.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# for each chordify, find the largest number different groups
found = 0
for c in self.data['chordify.flat.getElementsByClass(Chord)']:
# create a group to aggregate all groups for each pitch in this
# chord
g = base.Groups()
for p in c.pitches:
for gSub in p.groups:
g.append(gSub) # add to temporary group; will act as a set
found = max(found, len(g))
self.feature.vector[0] = found
[docs]
class AverageNumberOfIndependentVoicesFeature(featuresModule.FeatureExtractor):
'''
Average number of different channels in which notes have sounded simultaneously.
Rests are not included in this calculation. Here, Parts are treated as voices.
>>> s = corpus.parse('handel/rinaldo/lascia_chio_pianga')
>>> fe = features.jSymbolic.AverageNumberOfIndependentVoicesFeature(s)
>>> f = fe.extract()
>>> f.vector
[1.528...]
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.AverageNumberOfIndependentVoicesFeature(s)
>>> f = fe.extract()
>>> f.vector
[3.90...]
'''
id = 'T2'
name = 'Average Number of Independent Voices'
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.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# for each chordify, find the largest number different groups
found = []
for c in self.data['chordify.flat.getElementsByClass(Chord)']:
# create a group to aggregate all groups for each pitch in this
# chord
g = base.Groups()
for p in c.pitches:
for gSub in p.groups:
g.append(gSub) # add to temporary group; will act as a set
found.append(len(g))
if not found:
raise JSymbolicFeatureException('input lacks notes')
self.feature.vector[0] = sum(found) / len(found)
[docs]
class VariabilityOfNumberOfIndependentVoicesFeature(
featuresModule.FeatureExtractor):
'''
Standard deviation of number of different channels in which notes have sounded simultaneously.
Rests are not included in this calculation.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.jSymbolic.VariabilityOfNumberOfIndependentVoicesFeature(s)
>>> f = fe.extract()
>>> f.vector
[0.449...]
'''
id = 'T3'
name = 'Variability of Number of Independent Voices'
description = ('Standard deviation of number of different channels '
'in which notes have sounded simultaneously. Rests are '
'not included in this calculation.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
# for each chordify, find the largest number different groups
found = []
for c in self.data['chordify.flat.getElementsByClass(Chord)']:
# create a group to aggregate all groups for each pitch in this
# chord
g = base.Groups()
for p in c.pitches:
for gSub in p.groups:
g.append(gSub) # add to temporary group; will act as a set
found.append(len(g))
if found:
self.feature.vector[0] = statistics.pstdev(found)
else:
raise JSymbolicFeatureException('input lacks notes')
[docs]
class VoiceEqualityNumberOfNotesFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
Standard deviation of the total number of Note Ons in each channel
that contains at least one note.
'''
id = 'T4'
name = 'Voice Equality - Number of Notes'
description = ('Standard deviation of the total number of Note Ons '
'in each channel that contains at least one note.')
[docs]
class VoiceEqualityNoteDurationFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
'''
id = 'T5'
name = 'Voice Equality - Note Duration'
description = ('Standard deviation of the total duration of notes in seconds '
'in each channel that contains at least one note.')
[docs]
class VoiceEqualityDynamicsFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
'''
id = 'T6'
name = 'Voice Equality - Dynamics'
description = ('Standard deviation of the average volume of notes '
'in each channel that contains at least one note.')
[docs]
class VoiceEqualityMelodicLeapsFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
'''
id = 'T7'
name = 'Voice Equality - Melodic Leaps'
description = dedent('''
Standard deviation
of the average melodic leap in MIDI pitches
for each channel that contains at least one note.''')
[docs]
class VoiceEqualityRangeFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
Standard deviation of the differences between the highest and lowest
pitches in each channel that contains at least one note.
'''
id = 'T8'
name = 'Voice Equality - Range'
description = dedent('''
Standard deviation of the differences between the
highest and lowest pitches in each channel that contains at least one note.''')
[docs]
class ImportanceOfLoudestVoiceFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
'''
id = 'T9'
name = 'Importance of Loudest Voice'
description = dedent('''
Difference between the average loudness
of the loudest channel and the average loudness of the other channels
that contain at least one note.''')
[docs]
class RelativeRangeOfLoudestVoiceFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
'''
id = 'T10'
name = 'Relative Range of Loudest Voice'
description = dedent('''
Difference between the highest note and the lowest note
played in the channel with the highest average loudness divided by the difference
between the highest note and the lowest note overall in the piece.''')
[docs]
class RangeOfHighestLineFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
'''
id = 'T12'
name = 'Range of Highest Line'
description = dedent('''
Difference between the highest note and the lowest note
played in the channel with the highest average pitch divided by the difference
between the highest note and the lowest note in the piece.''')
[docs]
class RelativeNoteDensityOfHighestLineFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
'''
id = 'T13'
name = 'Relative Note Density of Highest Line'
description = dedent('''
Number of Note Ons in the channel with the highest average
pitch divided by the average number of Note Ons in all channels that contain at
least one note.''')
[docs]
class MelodicIntervalsInLowestLineFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
'''
id = 'T15'
name = 'Melodic Intervals in Lowest Line'
description = dedent('''
Average melodic interval in semitones of the channel
with the lowest average pitch divided by the average melodic interval of all
channels that contain at least two notes.''')
[docs]
class VoiceSeparationFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
Average separation in semitones between the average pitches of consecutive
channels (after sorting based/non-average pitch) that contain at least one note.
'''
id = 'T20'
name = 'Voice Separation'
description = dedent('''
Average separation in semi-tones between the average pitches of
consecutive channels (after sorting based/non average pitch) that contain at
least one note.''')
# ------------------------------------------------------------------------------
# instrumentation
[docs]
class PitchedInstrumentsPresentFeature(featuresModule.FeatureExtractor):
'''
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.
>>> s1 = stream.Stream()
>>> s1.append(instrument.AcousticGuitar())
>>> s1.append(note.Note())
>>> s1.append(instrument.Tuba())
>>> s1.append(note.Note())
>>> fe = features.jSymbolic.PitchedInstrumentsPresentFeature(s1)
>>> fe.extract().vector
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Default instruments will lack a `.midiProgram`, so they raise exceptions:
>>> i = instrument.Instrument()
>>> i.midiProgram is None
True
>>> s2 = stream.Stream()
>>> s2.append(i)
>>> s2.append(note.Note())
>>> fe2 = features.jSymbolic.PitchedInstrumentsPresentFeature(s2)
>>> fe2.extract()
Traceback (most recent call last):
music21.features.jSymbolic.JSymbolicFeatureException:
<music21.instrument.Instrument ''> lacks a midiProgram
'''
id = 'I1'
name = 'Pitched Instruments Present'
description = dedent('''
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.''')
dimensions = 128
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
s = self.data['partitionByInstrument']
# each part has content for each instrument
# count = 0
if not s:
raise JSymbolicFeatureException('input lacks instruments')
for p in s.parts:
# always one instrument
i = p.getElementsByClass(Instrument).first()
if p.recurse().notes:
if i.midiProgram is None:
iStr = str(i) or repr(i)
raise JSymbolicFeatureException(f'{iStr} lacks a midiProgram')
self.feature.vector[i.midiProgram] = 1
[docs]
class UnpitchedInstrumentsPresentFeature(featuresModule.FeatureExtractor):
'''
Not yet implemented.
Which unpitched MIDI Percussion Key Map 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. It should be noted that only instruments 35 to 81 are included here,
as they are the ones that meet the official standard. They are numbered in this
array from 0 to 46.
'''
# NOTE: this is incorrect: these are not instruments 35 to 81, but pitch
# values in for events on midi program channel 10
id = 'I2'
name = 'Unpitched Instruments Present'
description = dedent('''
Which unpitched MIDI Percussion Key Map 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.
It should be noted that only instruments 35 to 81 are included here, as they are the
ones that meet the official standard. They are numbered in this array from 0 to 46.''')
dimensions = 47
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
raise JSymbolicFeatureException('not yet implemented')
# TODO: implement
[docs]
class NotePrevalenceOfPitchedInstrumentsFeature(
featuresModule.FeatureExtractor):
'''
>>> s1 = stream.Stream()
>>> s1.append(instrument.AcousticGuitar())
>>> s1.repeatAppend(note.Note(), 4)
>>> s1.append(instrument.Tuba())
>>> s1.append(note.Note())
>>> fe = features.jSymbolic.NotePrevalenceOfPitchedInstrumentsFeature(s1)
>>> fe.extract().vector
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0.8..., 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2...,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
`.midiProgram` cannot be None:
>>> s1.getInstruments().first().midiProgram = None
>>> fe2 = features.jSymbolic.NotePrevalenceOfPitchedInstrumentsFeature(s1)
>>> fe2.extract()
Traceback (most recent call last):
music21.features.jSymbolic.JSymbolicFeatureException: Acoustic Guitar lacks a midiProgram
'''
id = 'I3'
name = 'Note Prevalence of Pitched Instruments'
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.')
dimensions = 128
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
s = self.data['partitionByInstrument']
total = sum(self.data['pitches.pitchClassHistogram'])
# each part has content for each instrument
# count = 0
if not s:
raise JSymbolicFeatureException('input lacks notes')
for p in s.parts:
# always one instrument
i = p.getElementsByClass(Instrument).first()
pNotes = p.recurse().notes
if pNotes:
if i.midiProgram is None:
iStr = str(i) or repr(i)
raise JSymbolicFeatureException(f'{iStr} lacks a midiProgram')
self.feature.vector[i.midiProgram] = len(pNotes) / total
[docs]
class NotePrevalenceOfUnpitchedInstrumentsFeature(
featuresModule.FeatureExtractor):
'''
Not implemented.
TODO: implement
'''
id = 'I4'
name = 'Note Prevalence of Unpitched Instruments'
description = dedent('''
The fraction of (unpitched) notes played by each General MIDI
Percussion Key Map Instrument. There is one entry for each instrument, which is set
to the number of Note Ons played using the corresponding MIDI note value divided by
the total number of Note Ons in the recording. It should be noted that only instruments
35 to 81 are included here, as they are the ones that meet the official standard.
They are numbered in this array from 0 to 46.''')
dimensions = 47
# TODO: need to find events in channel 10.
[docs]
class TimePrevalenceOfPitchedInstrumentsFeature(
featuresModule.FeatureExtractor):
'''
Not implemented.
The fraction of the total time of the recording in
which a note was sounding for each (pitched) General
MIDI Instrument. There is one entry for each instrument,
which is set to the total time in seconds during which a
given instrument was sounding one or more notes divided by the total length
in seconds of the piece.'
TODO: implement
'''
id = 'I5'
name = 'Time Prevalence of Pitched Instruments'
description = ('The fraction of the total time of the recording in which a note '
'was sounding for each (pitched) General MIDI Instrument. '
'There is one entry for each instrument, which is set to the total '
'time in seconds during which a given instrument was sounding one '
'or more notes divided by the total length in seconds of the piece.')
dimensions = 128
# TODO: this can be done by symbolic duration in native.py
[docs]
class VariabilityOfNotePrevalenceOfPitchedInstrumentsFeature(
featuresModule.FeatureExtractor):
'''
Standard deviation of the fraction of Note Ons played
by each (pitched) General MIDI instrument that is
used to play at least one note.
>>> s1 = stream.Stream()
>>> s1.append(instrument.AcousticGuitar())
>>> s1.repeatAppend(note.Note(), 5)
>>> s1.append(instrument.Tuba())
>>> s1.append(note.Note())
>>> fe = features.jSymbolic.VariabilityOfNotePrevalenceOfPitchedInstrumentsFeature(s1)
>>> fe.extract().vector
[0.33333...]
'''
id = 'I6'
name = 'Variability of Note Prevalence of Pitched Instruments'
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.')
[docs]
def process(self) -> None:
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
s = self.data['partitionByInstrument']
total = sum(self.data['pitches.pitchClassHistogram'])
if not s:
raise JSymbolicFeatureException('input lacks instruments')
if not total:
raise JSymbolicFeatureException('input lacks notes')
# each part has content for each instrument
coll = []
for p in s.parts:
# always one instrument
i = p.getElementsByClass(Instrument).first()
pNotes = p.recurse().notes
if pNotes:
coll.append(len(pNotes) / total)
# Would be faster to use numpy.
# numpy.std(coll)
mean = sum(coll) / len(coll)
# squared deviations from the mean
partial = [pow(n - mean, 2) for n in coll]
self.feature.vector[0] = math.sqrt(sum(partial) / len(partial))
[docs]
class VariabilityOfNotePrevalenceOfUnpitchedInstrumentsFeature(
featuresModule.FeatureExtractor):
'''
Not implemented.
Standard deviation of the fraction of Note Ons played by each (unpitched) MIDI Percussion Key
Map instrument that is used to play at least one note. It should be noted that only
instruments 35 to 81 are included here, as they are the ones that are included in the
official standard.
TODO: implement
'''
id = 'I7'
name = 'Variability of Note Prevalence of Unpitched Instruments'
description = (
'Standard deviation of the fraction of Note Ons played by each (unpitched) '
'MIDI Percussion Key Map instrument that is used to play at least one note. '
'It should be noted that only instruments 35 to 81 are included here, '
'as they are the ones that are included in the official standard.')
[docs]
class NumberOfPitchedInstrumentsFeature(featuresModule.FeatureExtractor):
'''
Total number of General MIDI patches that are used to play at least one note.
>>> s1 = stream.Stream()
>>> s1.append(instrument.AcousticGuitar())
>>> s1.append(note.Note())
>>> s1.append(instrument.Tuba())
>>> s1.append(note.Note())
>>> fe = features.jSymbolic.NumberOfPitchedInstrumentsFeature(s1)
>>> fe.extract().vector
[2]
'''
id = 'I8'
name = 'Number of Pitched Instruments'
description = ('Total number of General MIDI patches that are used to '
'play at least one note.')
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
s = self.data['partitionByInstrument']
# each part has content for each instrument
count = 0
if not s:
raise JSymbolicFeatureException('input lacks instruments')
for p in s.parts:
if p.recurse().notes:
count += 1
self.feature.vector[0] = count
[docs]
class NumberOfUnpitchedInstrumentsFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
Number of distinct MIDI Percussion Key Map patches that were used to play at
least one note. It should be noted that only instruments 35 to 81 are
included here, as they are the ones that are included in the official standard.
TODO: implement
'''
id = 'I9'
name = 'Number of Unpitched Instruments'
description = ('Number of distinct MIDI Percussion Key Map patches that were '
'used to play at least one note. It should be noted that only '
'instruments 35 to 81 are included here, as they are the ones '
'that are included in the official standard.')
[docs]
class PercussionPrevalenceFeature(featuresModule.FeatureExtractor):
'''
Not implemented.
Total number of Note Ons corresponding to unpitched percussion instruments
divided by the total number of Note Ons in the recording.
'''
id = 'I10'
name = 'Percussion Prevalence'
description = ('Total number of Note Ons corresponding to unpitched percussion '
'instruments divided by total number of Note Ons in the recording.')
[docs]
class InstrumentFractionFeature(featuresModule.FeatureExtractor):
'''
TODO: Add description of feature
This subclass is in-turn subclassed by all FeatureExtractors that
look at the proportional usage of an Instrument
'''
_targetPrograms: Sequence[int] = []
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
s = self.data['partitionByInstrument']
total = sum(self.data['pitches.pitchClassHistogram'])
count = 0
if not s:
raise JSymbolicFeatureException('input lacks instruments')
if not total:
raise JSymbolicFeatureException('input lacks notes')
for p in s.parts:
i = p.getElementsByClass(Instrument).first()
if i.midiProgram in self._targetPrograms:
count += len(p.recurse().notes)
self.feature.vector[0] = count / total
[docs]
class StringKeyboardFractionFeature(InstrumentFractionFeature):
'''
Fraction of all Note Ons belonging to string keyboard patches
(General MIDI patches 1 to 8).
>>> s1 = stream.Stream()
>>> s1.append(instrument.Piano())
>>> s1.repeatAppend(note.Note(), 9)
>>> s1.append(instrument.Tuba())
>>> s1.append(note.Note())
>>> fe = features.jSymbolic.StringKeyboardFractionFeature(s1)
>>> fe.extract().vector
[0.9...]
'''
id = 'I11'
name = 'String Keyboard Fraction'
description = ('Fraction of all Note Ons belonging to string keyboard patches '
'(General MIDI patches 1 to 8).')
_targetPrograms = range(8)
[docs]
class AcousticGuitarFractionFeature(InstrumentFractionFeature):
'''
A feature extractor that extracts the fraction of all Note Ons belonging to
acoustic guitar patches (General MIDI patches 25 and 26).
>>> s1 = stream.Stream()
>>> s1.append(instrument.AcousticGuitar())
>>> s1.repeatAppend(note.Note(), 3)
>>> s1.append(instrument.Tuba())
>>> s1.append(note.Note())
>>> fe = features.jSymbolic.AcousticGuitarFractionFeature(s1)
>>> fe.extract().vector
[0.75]
'''
id = 'I12'
name = 'Acoustic Guitar Fraction'
description = ('Fraction of all Note Ons belonging to acoustic guitar patches '
'(General MIDI patches 25 and 26).')
_targetPrograms = [24, 25]
[docs]
class ElectricGuitarFractionFeature(InstrumentFractionFeature):
'''
>>> s1 = stream.Stream()
>>> s1.append(instrument.ElectricGuitar())
>>> s1.repeatAppend(note.Note(), 4)
>>> s1.append(instrument.Tuba())
>>> s1.repeatAppend(note.Note(), 4)
>>> fe = features.jSymbolic.ElectricGuitarFractionFeature(s1)
>>> fe.extract().vector
[0.5]
'''
id = 'I13'
name = 'Electric Guitar Fraction'
description = ('Fraction of all Note Ons belonging to '
'electric guitar patches (General MIDI patches 27 to 32).')
_targetPrograms = list(range(26, 32))
[docs]
class ViolinFractionFeature(InstrumentFractionFeature):
'''
Fraction of all Note Ons belonging to violin patches (General MIDI
patches 41 or 111).
>>> s1 = stream.Stream()
>>> s1.append(instrument.Violin())
>>> s1.repeatAppend(note.Note(), 2)
>>> s1.append(instrument.Tuba())
>>> s1.repeatAppend(note.Note(), 8)
>>> fe = features.jSymbolic.ViolinFractionFeature(s1)
>>> fe.extract().vector
[0.2...]
'''
id = 'I14'
name = 'Violin Fraction'
description = ('Fraction of all Note Ons belonging to violin patches '
'(General MIDI patches 41 or 111).')
_targetPrograms = [40, 110]
[docs]
class SaxophoneFractionFeature(InstrumentFractionFeature):
'''
Fraction of all Note Ons belonging to saxophone patches (General MIDI
patches 65 through 68).
# NOTE: incorrect
>>> s1 = stream.Stream()
>>> s1.append(instrument.SopranoSaxophone())
>>> s1.repeatAppend(note.Note(), 6)
>>> s1.append(instrument.Tuba())
>>> s1.repeatAppend(note.Note(), 4)
>>> fe = features.jSymbolic.SaxophoneFractionFeature(s1)
>>> print(fe.extract().vector[0])
0.6
'''
id = 'I15'
name = 'Saxophone Fraction'
description = ('Fraction of all Note Ons belonging to saxophone patches '
'(General MIDI patches 65 through 68).')
_targetPrograms = [64, 65, 66, 67]
[docs]
class BrassFractionFeature(InstrumentFractionFeature):
'''
A feature extractor that extracts the fraction of all Note Ons
belonging to brass patches (General MIDI patches 57 through 68).
TODO: Conflict in source: only does 57-62?
>>> s1 = stream.Stream()
>>> s1.append(instrument.SopranoSaxophone())
>>> s1.repeatAppend(note.Note(), 6)
>>> s1.append(instrument.Tuba())
>>> s1.repeatAppend(note.Note(), 4)
>>> fe = features.jSymbolic.BrassFractionFeature(s1)
>>> print(fe.extract().vector[0])
0.4
'''
id = 'I16'
name = 'Brass Fraction'
description = ('Fraction of all Note Ons belonging to brass patches '
'(General MIDI patches 57 through 68).') # note: incorrect
_targetPrograms = list(range(56, 62))
[docs]
class WoodwindsFractionFeature(InstrumentFractionFeature):
'''
Fraction of all Note Ons belonging to woodwind patches
(General MIDI patches 69 through 76).
TODO: Conflict in source: does 69-79?
>>> s1 = stream.Stream()
>>> s1.append(instrument.Flute())
>>> s1.repeatAppend(note.Note(), 3)
>>> s1.append(instrument.Tuba())
>>> s1.repeatAppend(note.Note(), 7)
>>> fe = features.jSymbolic.WoodwindsFractionFeature(s1)
>>> print(fe.extract().vector[0])
0.3
'''
id = 'I17'
name = 'Woodwinds Fraction'
description = ('Fraction of all Note Ons belonging to woodwind patches '
'(General MIDI patches 69 through 76).')
_targetPrograms = list(range(68, 80)) # include ocarina!
[docs]
class OrchestralStringsFractionFeature(InstrumentFractionFeature):
'''
Fraction of all Note Ons belonging to orchestral strings patches
(General MIDI patches 41 or 47).
>>> s1 = stream.Stream()
>>> s1.append(instrument.Violoncello())
>>> s1.repeatAppend(note.Note(), 4)
>>> s1.append(instrument.Tuba())
>>> s1.repeatAppend(note.Note(), 6)
>>> fe = features.jSymbolic.OrchestralStringsFractionFeature(s1)
>>> print(fe.extract().vector[0])
0.4
'''
id = 'I18'
name = 'Orchestral Strings Fraction'
description = ('Fraction of all Note Ons belonging to orchestral strings patches '
'(General MIDI patches 41 or 47).')
_targetPrograms = list(range(41, 46))
[docs]
class StringEnsembleFractionFeature(InstrumentFractionFeature):
'''
Not implemented.
Fraction of all Note Ons belonging to string ensemble patches
(General MIDI patches 49 to 52).
'''
# TODO: add tests, do not yet have instrument to model
id = 'I19'
name = 'String Ensemble Fraction'
description = ('Fraction of all Note Ons belonging to string ensemble patches '
'(General MIDI patches 49 to 52).')
_targetPrograms = [48, 49, 50, 51]
[docs]
class ElectricInstrumentFractionFeature(InstrumentFractionFeature):
'''
Fraction of all Note Ons belonging to electric instrument patches
(General MIDI patches 5, 6, 17, 19, 27 through 32, 24 through 40).
>>> s1 = stream.Stream()
>>> s1.append(instrument.ElectricOrgan())
>>> s1.repeatAppend(note.Note(), 8)
>>> s1.append(instrument.Tuba())
>>> s1.repeatAppend(note.Note(), 2)
>>> fe = features.jSymbolic.ElectricInstrumentFractionFeature(s1)
>>> print(fe.extract().vector[0])
0.8
'''
id = 'I20'
name = 'Electric Instrument Fraction'
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).')
_targetPrograms = [4, 5, 16, 18, 26, 27, 28, 29,
30, 31, 33, 34, 35, 36, 37, 38, 39] # accept synth bass
# -----------------------------------------------------------------------------
class JSymbolicFeatureException(featuresModule.FeatureException):
pass
extractorsById = OrderedDict([
('D', [
None,
OverallDynamicRangeFeature,
VariationOfDynamicsFeature,
VariationOfDynamicsInEachVoiceFeature,
AverageNoteToNoteDynamicsChangeFeature,
]),
('I', [
None,
PitchedInstrumentsPresentFeature,
UnpitchedInstrumentsPresentFeature,
NotePrevalenceOfPitchedInstrumentsFeature,
NotePrevalenceOfUnpitchedInstrumentsFeature,
TimePrevalenceOfPitchedInstrumentsFeature,
VariabilityOfNotePrevalenceOfPitchedInstrumentsFeature,
VariabilityOfNotePrevalenceOfUnpitchedInstrumentsFeature,
NumberOfPitchedInstrumentsFeature,
NumberOfUnpitchedInstrumentsFeature,
PercussionPrevalenceFeature,
StringKeyboardFractionFeature,
AcousticGuitarFractionFeature,
ElectricGuitarFractionFeature,
ViolinFractionFeature,
SaxophoneFractionFeature,
BrassFractionFeature,
WoodwindsFractionFeature,
OrchestralStringsFractionFeature,
StringEnsembleFractionFeature,
ElectricInstrumentFractionFeature,
]),
('M', [
None,
MelodicIntervalHistogramFeature,
AverageMelodicIntervalFeature,
MostCommonMelodicIntervalFeature,
DistanceBetweenMostCommonMelodicIntervalsFeature,
MostCommonMelodicIntervalPrevalenceFeature,
RelativeStrengthOfMostCommonIntervalsFeature,
NumberOfCommonMelodicIntervalsFeature,
AmountOfArpeggiationFeature,
RepeatedNotesFeature,
ChromaticMotionFeature,
StepwiseMotionFeature,
MelodicThirdsFeature,
MelodicFifthsFeature,
MelodicTritonesFeature,
MelodicOctavesFeature,
None, # EmbellishmentFeature,
DirectionOfMotionFeature,
DurationOfMelodicArcsFeature,
SizeOfMelodicArcsFeature,
None, # MelodicPitchVarietyFeature,
]),
('P', [
None,
MostCommonPitchPrevalenceFeature,
MostCommonPitchClassPrevalenceFeature,
RelativeStrengthOfTopPitchesFeature,
RelativeStrengthOfTopPitchClassesFeature,
IntervalBetweenStrongestPitchesFeature,
IntervalBetweenStrongestPitchClassesFeature,
NumberOfCommonPitchesFeature,
PitchVarietyFeature,
PitchClassVarietyFeature,
RangeFeature,
MostCommonPitchFeature,
PrimaryRegisterFeature,
ImportanceOfBassRegisterFeature,
ImportanceOfMiddleRegisterFeature,
ImportanceOfHighRegisterFeature,
MostCommonPitchClassFeature,
DominantSpreadFeature,
StrongTonalCentresFeature,
BasicPitchHistogramFeature,
PitchClassDistributionFeature,
FifthsPitchHistogramFeature,
QualityFeature,
GlissandoPrevalenceFeature,
AverageRangeOfGlissandosFeature,
VibratoPrevalenceFeature,
None, # PrevalenceOfMicroTonesFeature,
]),
('R', [
None,
StrongestRhythmicPulseFeature,
SecondStrongestRhythmicPulseFeature,
HarmonicityOfTwoStrongestRhythmicPulsesFeature,
StrengthOfStrongestRhythmicPulseFeature,
StrengthOfSecondStrongestRhythmicPulseFeature,
StrengthRatioOfTwoStrongestRhythmicPulsesFeature,
CombinedStrengthOfTwoStrongestRhythmicPulsesFeature,
NumberOfStrongPulsesFeature,
NumberOfModeratePulsesFeature,
NumberOfRelativelyStrongPulsesFeature,
RhythmicLoosenessFeature,
PolyrhythmsFeature,
RhythmicVariabilityFeature,
BeatHistogramFeature,
NoteDensityFeature,
None, # NoteDensityVariabilityFeature
AverageNoteDurationFeature,
VariabilityOfNoteDurationFeature,
MaximumNoteDurationFeature,
MinimumNoteDurationFeature,
StaccatoIncidenceFeature,
AverageTimeBetweenAttacksFeature,
VariabilityOfTimeBetweenAttacksFeature,
AverageTimeBetweenAttacksForEachVoiceFeature,
AverageVariabilityOfTimeBetweenAttacksForEachVoiceFeature,
None, # IncidenceOfCompleteRestsFeature,
None, # MaximumCompleteRestDurationFeature,
None, # AverageRestDurationPerVoiceFeature,
None, # AverageVariabilityOfRestDurationsAcrossVoicesFeature,
InitialTempoFeature,
InitialTimeSignatureFeature,
CompoundOrSimpleMeterFeature,
TripleMeterFeature,
QuintupleMeterFeature,
ChangesOfMeterFeature,
DurationFeature,
]),
('T', [
None,
MaximumNumberOfIndependentVoicesFeature,
AverageNumberOfIndependentVoicesFeature,
VariabilityOfNumberOfIndependentVoicesFeature,
VoiceEqualityNumberOfNotesFeature,
VoiceEqualityNoteDurationFeature,
VoiceEqualityDynamicsFeature,
VoiceEqualityMelodicLeapsFeature,
VoiceEqualityRangeFeature,
ImportanceOfLoudestVoiceFeature,
RelativeRangeOfLoudestVoiceFeature,
None, # RelativeRangeIsolationOfLoudestVoiceFeature,
RangeOfHighestLineFeature,
RelativeNoteDensityOfHighestLineFeature,
None, # RelativeNoteDurationsOfLowestLineFeature
MelodicIntervalsInLowestLineFeature,
None, # SimultaneityFeature
None, # VariabilityOfSimultaneityFeature
None, # VoiceOverlapFeature
None, # ParallelMotionFeature
VoiceSeparationFeature,
]),
('C', [
None,
None, # VerticalIntervalsFeature,
None, # ChordTypesFeature,
None, # MostCommonVerticalIntervalFeature,
None, # SecondMostCommonVerticalIntervalFeature,
None, # DistanceBetweenTwoMostCommonVerticalIntervalsFeature,
None, # PrevalenceOfMostCommonVerticalIntervalFeature,
None, # PrevalenceOfSecondMostCommonVerticalIntervalFeature,
None, # RatioOfPrevalenceOfTwoMostCommonVerticalIntervalsFeature,
None, # AverageNumberOfSimultaneousPitchClassesFeature,
None, # VariabilityOfNumberOfSimultaneousPitchClassesFeature,
None, # MinorMajorRatioFeature,
None, # PerfectVerticalIntervalsFeature,
None, # UnisonsFeature,
None, # VerticalMinorSecondsFeature,
None, # VerticalThirdsFeature,
None, # VerticalFifthsFeature,
None, # VerticalTritonesFeature,
None, # VerticalOctavesFeature,
None, # VerticalDissonanceRatioFeature,
None, # PartialChordsFeature,
None, # MinorMajorTriadRatioFeature,
None, # StandardTriadsFeature,
None, # DiminishedAndAugmentedTriadsFeature,
None, # DominantSeventhChordsFeature,
None, # SeventhsChordsFeature,
None, # ComplexChordsFeature,
None, # NonStandardChordsFeature,
None, # ChordDurationFeature,
]),
])
[docs]
def getExtractorByTypeAndNumber(extractorType, number):
'''
Typical usage:
>>> t5 = features.jSymbolic.getExtractorByTypeAndNumber('T', 5)
>>> t5.__name__
'VoiceEqualityNoteDurationFeature'
>>> bachExample = corpus.parse('bach/bwv66.6')
>>> fe = t5(bachExample)
Features unimplemented in jSymbolic but documented in the dissertation return None
>>> features.jSymbolic.getExtractorByTypeAndNumber('C', 20) is None
True
Totally unknown features return an exception:
>>> features.jSymbolic.getExtractorByTypeAndNumber('L', 900)
Traceback (most recent call last):
music21.features.jSymbolic.JSymbolicFeatureException: Could not find
any jSymbolic features of type L
>>> features.jSymbolic.getExtractorByTypeAndNumber('C', 200)
Traceback (most recent call last):
music21.features.jSymbolic.JSymbolicFeatureException: jSymbolic
features of type C do not have number 200
You could also find all the feature extractors this way:
>>> fs = features.jSymbolic.extractorsById
>>> for k in fs:
... for i in range(len(fs[k])):
... if fs[k][i] is not None:
... n = fs[k][i].__name__
... if fs[k][i] not in features.jSymbolic.featureExtractors:
... n += ' (not implemented)'
... print(f'{k} {i} {n}')
D 1 OverallDynamicRangeFeature (not implemented)
D 2 VariationOfDynamicsFeature (not implemented)
D 3 VariationOfDynamicsInEachVoiceFeature (not implemented)
D 4 AverageNoteToNoteDynamicsChangeFeature (not implemented)
I 1 PitchedInstrumentsPresentFeature
I 2 UnpitchedInstrumentsPresentFeature (not implemented)
I 3 NotePrevalenceOfPitchedInstrumentsFeature
I 4 NotePrevalenceOfUnpitchedInstrumentsFeature (not implemented)
I 5 TimePrevalenceOfPitchedInstrumentsFeature (not implemented)
I 6 VariabilityOfNotePrevalenceOfPitchedInstrumentsFeature
I 7 VariabilityOfNotePrevalenceOfUnpitchedInstrumentsFeature (not implemented)
I 8 NumberOfPitchedInstrumentsFeature
I 9 NumberOfUnpitchedInstrumentsFeature (not implemented)
I 10 PercussionPrevalenceFeature (not implemented)
I 11 StringKeyboardFractionFeature
I 12 AcousticGuitarFractionFeature
I 13 ElectricGuitarFractionFeature
I 14 ViolinFractionFeature
I 15 SaxophoneFractionFeature
I 16 BrassFractionFeature
I 17 WoodwindsFractionFeature
I 18 OrchestralStringsFractionFeature
I 19 StringEnsembleFractionFeature
I 20 ElectricInstrumentFractionFeature
M 1 MelodicIntervalHistogramFeature
M 2 AverageMelodicIntervalFeature
M 3 MostCommonMelodicIntervalFeature
M 4 DistanceBetweenMostCommonMelodicIntervalsFeature
M 5 MostCommonMelodicIntervalPrevalenceFeature
M 6 RelativeStrengthOfMostCommonIntervalsFeature
M 7 NumberOfCommonMelodicIntervalsFeature
M 8 AmountOfArpeggiationFeature
M 9 RepeatedNotesFeature
M 10 ChromaticMotionFeature
M 11 StepwiseMotionFeature
M 12 MelodicThirdsFeature
M 13 MelodicFifthsFeature
M 14 MelodicTritonesFeature
M 15 MelodicOctavesFeature
M 17 DirectionOfMotionFeature
M 18 DurationOfMelodicArcsFeature
M 19 SizeOfMelodicArcsFeature
P 1 MostCommonPitchPrevalenceFeature
P 2 MostCommonPitchClassPrevalenceFeature
P 3 RelativeStrengthOfTopPitchesFeature
P 4 RelativeStrengthOfTopPitchClassesFeature
P 5 IntervalBetweenStrongestPitchesFeature
P 6 IntervalBetweenStrongestPitchClassesFeature
P 7 NumberOfCommonPitchesFeature
P 8 PitchVarietyFeature
P 9 PitchClassVarietyFeature
P 10 RangeFeature
P 11 MostCommonPitchFeature
P 12 PrimaryRegisterFeature
P 13 ImportanceOfBassRegisterFeature
P 14 ImportanceOfMiddleRegisterFeature
P 15 ImportanceOfHighRegisterFeature
P 16 MostCommonPitchClassFeature
P 17 DominantSpreadFeature (not implemented)
P 18 StrongTonalCentresFeature (not implemented)
P 19 BasicPitchHistogramFeature
P 20 PitchClassDistributionFeature
P 21 FifthsPitchHistogramFeature
P 22 QualityFeature
P 23 GlissandoPrevalenceFeature (not implemented)
P 24 AverageRangeOfGlissandosFeature (not implemented)
P 25 VibratoPrevalenceFeature (not implemented)
R 1 StrongestRhythmicPulseFeature (not implemented)
R 2 SecondStrongestRhythmicPulseFeature (not implemented)
R 3 HarmonicityOfTwoStrongestRhythmicPulsesFeature (not implemented)
R 4 StrengthOfStrongestRhythmicPulseFeature (not implemented)
R 5 StrengthOfSecondStrongestRhythmicPulseFeature (not implemented)
R 6 StrengthRatioOfTwoStrongestRhythmicPulsesFeature (not implemented)
R 7 CombinedStrengthOfTwoStrongestRhythmicPulsesFeature (not implemented)
R 8 NumberOfStrongPulsesFeature (not implemented)
R 9 NumberOfModeratePulsesFeature (not implemented)
R 10 NumberOfRelativelyStrongPulsesFeature (not implemented)
R 11 RhythmicLoosenessFeature (not implemented)
R 12 PolyrhythmsFeature (not implemented)
R 13 RhythmicVariabilityFeature (not implemented)
R 14 BeatHistogramFeature (not implemented)
R 15 NoteDensityFeature
R 17 AverageNoteDurationFeature
R 18 VariabilityOfNoteDurationFeature
R 19 MaximumNoteDurationFeature
R 20 MinimumNoteDurationFeature
R 21 StaccatoIncidenceFeature
R 22 AverageTimeBetweenAttacksFeature
R 23 VariabilityOfTimeBetweenAttacksFeature
R 24 AverageTimeBetweenAttacksForEachVoiceFeature
R 25 AverageVariabilityOfTimeBetweenAttacksForEachVoiceFeature
R 30 InitialTempoFeature
R 31 InitialTimeSignatureFeature
R 32 CompoundOrSimpleMeterFeature
R 33 TripleMeterFeature
R 34 QuintupleMeterFeature
R 35 ChangesOfMeterFeature
R 36 DurationFeature
T 1 MaximumNumberOfIndependentVoicesFeature
T 2 AverageNumberOfIndependentVoicesFeature
T 3 VariabilityOfNumberOfIndependentVoicesFeature
T 4 VoiceEqualityNumberOfNotesFeature (not implemented)
T 5 VoiceEqualityNoteDurationFeature (not implemented)
T 6 VoiceEqualityDynamicsFeature (not implemented)
T 7 VoiceEqualityMelodicLeapsFeature (not implemented)
T 8 VoiceEqualityRangeFeature (not implemented)
T 9 ImportanceOfLoudestVoiceFeature (not implemented)
T 10 RelativeRangeOfLoudestVoiceFeature (not implemented)
T 12 RangeOfHighestLineFeature (not implemented)
T 13 RelativeNoteDensityOfHighestLineFeature (not implemented)
T 15 MelodicIntervalsInLowestLineFeature (not implemented)
T 20 VoiceSeparationFeature (not implemented)
'''
try:
return extractorsById[extractorType][number]
except KeyError:
raise JSymbolicFeatureException(
f'Could not find any jSymbolic features of type {extractorType}')
except IndexError:
raise JSymbolicFeatureException(
f'jSymbolic features of type {extractorType} do not have number {number}')
featureExtractors = [
MelodicIntervalHistogramFeature, # m1
AverageMelodicIntervalFeature, # m2
MostCommonMelodicIntervalFeature, # m3
DistanceBetweenMostCommonMelodicIntervalsFeature, # m4
MostCommonMelodicIntervalPrevalenceFeature, # m5
RelativeStrengthOfMostCommonIntervalsFeature, # m6
NumberOfCommonMelodicIntervalsFeature, # m7
AmountOfArpeggiationFeature, # m8
RepeatedNotesFeature, # m9
ChromaticMotionFeature, # m10
StepwiseMotionFeature, # m11
MelodicThirdsFeature, # m12
MelodicFifthsFeature, # m13
MelodicTritonesFeature, # m14
MelodicOctavesFeature, # m15
DirectionOfMotionFeature, # m17
DurationOfMelodicArcsFeature, # m18
SizeOfMelodicArcsFeature, # m 19
PitchedInstrumentsPresentFeature, # i1
NotePrevalenceOfPitchedInstrumentsFeature, # i3
VariabilityOfNotePrevalenceOfPitchedInstrumentsFeature, # i6
NumberOfPitchedInstrumentsFeature, # i8
StringKeyboardFractionFeature, # i11
AcousticGuitarFractionFeature, # i12
ElectricGuitarFractionFeature, # i13
ViolinFractionFeature, # i14
SaxophoneFractionFeature, # i15
BrassFractionFeature, # i16
WoodwindsFractionFeature, # i17
OrchestralStringsFractionFeature, # i18
StringEnsembleFractionFeature, # i19
ElectricInstrumentFractionFeature, # i20
# t11 not in jSymbolic
# t14 not in jSymbolic
# t16-19 not in jSymbolic
NoteDensityFeature, # r15
AverageNoteDurationFeature, # r17
VariabilityOfNoteDurationFeature, # r18
MaximumNoteDurationFeature, # r19
MinimumNoteDurationFeature, # r20
StaccatoIncidenceFeature, # r21
AverageTimeBetweenAttacksFeature, # r22
VariabilityOfTimeBetweenAttacksFeature, # r23
AverageTimeBetweenAttacksForEachVoiceFeature, # r24
AverageVariabilityOfTimeBetweenAttacksForEachVoiceFeature, # r25
# r26-29 not in jSymbolic
InitialTempoFeature, # r30
InitialTimeSignatureFeature, # r31
CompoundOrSimpleMeterFeature, # r32
TripleMeterFeature, # r33
QuintupleMeterFeature, # r34
ChangesOfMeterFeature, # r35
DurationFeature, # r36
MaximumNumberOfIndependentVoicesFeature, # t1
AverageNumberOfIndependentVoicesFeature, # t2
VariabilityOfNumberOfIndependentVoicesFeature, # t3
MostCommonPitchPrevalenceFeature, # p1
MostCommonPitchClassPrevalenceFeature, # p2
RelativeStrengthOfTopPitchesFeature, # p3
RelativeStrengthOfTopPitchClassesFeature, # p4
IntervalBetweenStrongestPitchesFeature, # p5
IntervalBetweenStrongestPitchClassesFeature, # p6
NumberOfCommonPitchesFeature, # p7
PitchVarietyFeature, # p8
PitchClassVarietyFeature, # p9
RangeFeature, # p10
MostCommonPitchFeature, # p11
PrimaryRegisterFeature, # p12
ImportanceOfBassRegisterFeature, # p13
ImportanceOfMiddleRegisterFeature, # p14
ImportanceOfHighRegisterFeature, # p15
MostCommonPitchClassFeature, # p16
BasicPitchHistogramFeature, # p19
PitchClassDistributionFeature, # p20
FifthsPitchHistogramFeature, # p21
QualityFeature, # p22
# p26 is not in jSymbolic
# m16 is not in jSymbolic
# m20 is not in jSymbolic
# c types are not in jSymbolic
]
[docs]
def getCompletionStats():
'''
>>> features.jSymbolic.getCompletionStats()
completion stats: 72/112 (0.6428...)
'''
countTotal = 0
countComplete = 0
for k in extractorsById: # a dictionary of lists
group = extractorsById[k]
for i in range(len(group)):
if group[i] is not None:
unused_n = group[i].__name__
countTotal += 1
if group[i] in featureExtractors:
countComplete += 1
print(f'completion stats: {countComplete}/{countTotal} ({countComplete / countTotal})')
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
def testAverageMelodicIntervalFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['p5', 'p5', 'p5', 'p4', 'p4', 'p4']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.AverageMelodicIntervalFeature(s)
f = fe.extract()
# average of 3 p5 and 3 p4 is the tritone
self.assertEqual(f.vector, [6.0])
def testMostCommonMelodicIntervalFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['p5', 'p5', 'p5', 'p4', 'p4', 'p4', 'p4']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.MostCommonMelodicIntervalFeature(s)
f = fe.extract()
# average of 3 p5 and 3 p4 is the tritone
self.assertEqual(f.vector, [5])
def testDistanceBetweenMostCommonMelodicIntervalsFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['p5', 'p5', 'p5', 'p4', 'p4', 'p4', 'p4', 'm2', 'm3']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.DistanceBetweenMostCommonMelodicIntervalsFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [2])
def testMostCommonMelodicIntervalPrevalenceFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['p5', 'p5', 'p4', 'p4', 'p4', 'p4', 'm2', 'm3']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.MostCommonMelodicIntervalPrevalenceFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [0.5])
def testRelativeStrengthOfMostCommonIntervalsFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['p5', 'p5', 'p5', 'p4', 'p4', 'p4', 'p4', 'm2', 'm3']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.RelativeStrengthOfMostCommonIntervalsFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [0.75])
def testNumberOfCommonMelodicIntervalsFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['p5', 'p4', 'p4', 'p4', 'p4', 'p4', 'p4', 'p4', 'p4', 'p4', 'p4', 'm2', 'm3']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.NumberOfCommonMelodicIntervalsFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [1])
def testAmountOfArpeggiationFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'M2', 'M3', 'p5', 'p8']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.AmountOfArpeggiationFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [0.5])
def testRepeatedNotesFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['p1', 'p1', 'p1', 'M2', 'M3', 'p5']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.RepeatedNotesFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [0.5])
def testChromaticMotionFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'm2', 'M2', 'M3', 'p5']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.ChromaticMotionFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [0.5])
def testStepwiseMotionFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'm2', 'M2', 'M3', 'p5']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.StepwiseMotionFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [2 / 3])
def testMelodicThirdsFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'm2', 'M2', 'M3', 'p5']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.MelodicThirdsFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [1 / 6])
def testMelodicFifthsFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'm2', 'M2', 'p5', 'p5']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.MelodicFifthsFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [2 / 6])
def testMelodicTritonesFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'm2', 'M2', 'p5', 'd5']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.MelodicTritonesFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [1 / 6])
def testMelodicOctavesFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'm2', 'M2', 'p5', 'p8']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.MelodicOctavesFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [1 / 6])
def testDirectionOfMotionFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
# all up
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'm2', 'M2', 'p5', 'p8']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.DirectionOfMotionFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [1.0])
# half down, half up
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', '-m2', '-M2', '-p5', 'p8']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.DirectionOfMotionFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [0.5])
# downward only
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['-m2', '-m2', '-m2', '-M2', '-p5', '-p8']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.DirectionOfMotionFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [0.0])
def testDurationOfMelodicArcsFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
# all up
# in jSymbolic implementation, all up means there
# is no melodic arc and thus the average duration
# of melodic arc is set to 0.
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'm2', 'M2', 'p5', 'p8']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.DurationOfMelodicArcsFeature(s)
f = fe.extract()
self.assertEqual(f.vector, [0])
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', '-p8', 'M2', 'p5', '-p8', 'p8', '-p8']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.DurationOfMelodicArcsFeature(s)
f = fe.extract()
self.assertAlmostEqual(f.vector[0], 8 / 5)
def testSizeOfMelodicArcsFeature(self):
from music21 import stream
from music21 import pitch
from music21 import note
from music21 import features
# all up
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', 'm2', 'M2', 'p5', 'p8']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.SizeOfMelodicArcsFeature(s)
unused_f = fe.extract()
# self.assertEqual(f.vector, [5])
s = stream.Stream()
p = pitch.Pitch('c2')
s.append(note.Note(copy.deepcopy(p)))
for i in ['m2', 'm2', '-p8', 'M2', 'p5', '-p8', 'p8', '-p8']:
p = p.transpose(i)
s.append(note.Note(copy.deepcopy(p)))
fe = features.jSymbolic.SizeOfMelodicArcsFeature(s)
unused_f = fe.extract()
# self.assertAlmostEqual(f.vector[0], 1 + 2/3)
def testNoteDensityFeatureA(self):
from music21 import stream
from music21 import note
from music21 import tempo
from music21 import features
s = stream.Stream()
s.insert(0, tempo.MetronomeMark(number=60))
s.insert(0, note.Note(quarterLength=8))
s.insert(1, note.Note(quarterLength=7))
s.insert(2, note.Note(quarterLength=6))
s.insert(3, note.Note(quarterLength=5))
s.insert(4, note.Note(quarterLength=4))
s.insert(5, note.Note(quarterLength=3))
s.insert(6, note.Note(quarterLength=2))
s.insert(7, note.Note(quarterLength=1))
fe = features.jSymbolic.NoteDensityFeature(s)
f = fe.extract()
# 8 notes in the span of 8 seconds
self.assertAlmostEqual(f.vector[0], 1.0)
s = stream.Stream()
s.insert(0, tempo.MetronomeMark(number=240))
s.insert(0, note.Note(quarterLength=8))
s.insert(1, note.Note(quarterLength=7))
s.insert(2, note.Note(quarterLength=6))
s.insert(3, note.Note(quarterLength=5))
s.insert(4, note.Note(quarterLength=4))
s.insert(5, note.Note(quarterLength=3))
s.insert(6, note.Note(quarterLength=2))
s.insert(7, note.Note(quarterLength=1))
# 8 notes in the span of 2 seconds
fe = features.jSymbolic.NoteDensityFeature(s)
f = fe.extract()
self.assertAlmostEqual(f.vector[0], 4.0)
def testFeatureCount(self):
from music21 import features
fs = features.jSymbolic.extractorsById
feTotal = 0
feImplemented = 0
for k in fs:
for i in range(len(fs[k])):
if fs[k][i] is not None:
feTotal += 1
if fs[k][i] in features.jSymbolic.featureExtractors:
feImplemented += 1
environLocal.printDebug(['fe total:', feTotal, 'fe implemented',
feImplemented, 'percent', feImplemented / feTotal])
def testBeatHistogram(self):
from music21 import corpus
from music21 import tempo
sch = corpus.parse('schoenberg/opus19', 2)
for p in sch.parts:
p.insert(0, tempo.MetronomeMark('Langsam', 70))
fe = StrongestRhythmicPulseFeature(sch)
f = fe.extract()
self.assertEqual(140.0, f.vector[0])
if __name__ == '__main__':
import music21
music21.mainTest(Test)