# ------------------------------------------------------------------------------
# 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"
'''
from __future__ import annotations
from collections import OrderedDict
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Melodic Interval Histogram'
self.description = ('A features array with bins corresponding to '
'the values of the melodic interval histogram.')
self.isSequential = True
self.dimensions = 128
self.normalize = True
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Average Melodic Interval'
self.description = 'Average melodic interval (in semitones).'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Melodic Interval'
self.description = 'Melodic interval with the highest frequency.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Distance Between Most Common Melodic Intervals'
self.description = ('Absolute value of the difference between the '
'most common melodic interval and the second most '
'common melodic interval.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Melodic Interval Prevalence'
self.description = 'Fraction of melodic intervals that belong to the most common interval.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Relative Strength of Most Common Intervals'
self.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.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Number of Common Melodic Intervals'
self.description = ('Number of melodic intervals that represent '
'at least 9% of all melodic intervals.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Amount of Arpeggiation'
self.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.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Repeated Notes'
self.description = 'Fraction of notes that are repeated melodically.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Chromatic Motion'
self.description = 'Fraction of melodic intervals corresponding to a semi-tone.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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 corresponded 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Stepwise Motion'
self.description = ('Fraction of melodic intervals that corresponded '
'to a minor or major second.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Melodic Thirds'
self.description = 'Fraction of melodic intervals that are major or minor thirds.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Melodic Fifths'
self.description = 'Fraction of melodic intervals that are perfect fifths.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Melodic Tritones'
self.description = 'Fraction of melodic intervals that are tritones.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Melodic Octaves'
self.description = 'Fraction of melodic intervals that are octaves.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Direction of Motion'
self.description = 'Fraction of melodic intervals that are rising rather than falling.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Duration of Melodic Arcs'
self.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.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Size of Melodic Arcs'
self.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.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Pitch Prevalence'
self.description = 'Fraction of Note Ons corresponding to the most common pitch.'
self.isSequential = True
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Pitch Class Prevalence'
self.description = 'Fraction of Note Ons corresponding to the most common pitch class.'
self.isSequential = True
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Relative Strength of Top Pitches'
self.description = ('The frequency of the 2nd most common pitch '
'divided by the frequency of the most common pitch.')
self.isSequential = True
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Relative Strength of Top Pitch Classes'
self.description = ('The frequency of the 2nd most common pitch class '
'divided by the frequency of the most common pitch class.')
self.isSequential = True
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Interval Between Strongest Pitches'
self.description = ('Absolute value of the difference between '
'the pitches of the two most common MIDI pitches.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Interval Between Strongest Pitch Classes'
self.description = ('Absolute value of the difference between the pitch '
'classes of the two most common MIDI pitch classes.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Number of Common Pitches'
self.description = ('Number of pitches that account individually '
'for at least 9% of all notes.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Pitch Variety'
self.description = 'Number of pitches used at least once.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Pitch Class Variety'
self.description = 'Number of pitch classes used at least once.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Range'
self.description = 'Difference between highest and lowest pitches.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Pitch'
self.description = 'Bin label of the most common pitch.'
self.isSequential = True
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Primary Register'
self.description = 'Average MIDI pitch.'
self.isSequential = True
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Importance of Bass Register'
self.description = 'Fraction of Note Ons between MIDI pitches 0 and 54.'
self.isSequential = True
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Importance of Middle Register'
self.description = 'Fraction of Note Ons between MIDI pitches 55 and 72.'
self.isSequential = True
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Importance of High Register'
self.description = 'Fraction of Note Ons between MIDI pitches 73 and 127.'
self.isSequential = True
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Pitch Class'
self.description = 'Bin label of the most common pitch class.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Dominant Spread'
self.description = ('Largest number of consecutive pitch classes separated by '
'perfect 5ths that accounted for at least 9% each of the notes.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Strong Tonal Centres'
self.description = ('Number of peaks in the fifths pitch histogram that each account '
'for at least 9% of all Note Ons.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Basic Pitch Histogram'
self.description = ('A features array with bins corresponding to the '
'values of the basic pitch histogram.')
self.isSequential = True
self.dimensions = 128
self.normalize = True
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Pitch Class Distribution'
self.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.')
self.isSequential = True
self.dimensions = 12
self.discrete = False
self.normalize = True
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Fifths Pitch Histogram'
self.description = ('A feature array with bins corresponding to the values of the '
'5ths pitch class histogram.')
self.isSequential = True
self.dimensions = 12
self.normalize = True
# create pc to index mapping
self._mapping = {}
for i in range(12):
self._mapping[i] = (7 * i) % 12
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Quality'
self.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.
'''
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Glissando Prevalence'
self.description = ('Number of Note Ons that have at least one MIDI Pitch Bend '
'associated with them divided by total number of pitched Note Ons.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Average Range Of Glissandos'
self.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.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Vibrato Prevalence'
self.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.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream,
**keywords)
self.name = 'Prevalence Of Microtones'
self.description = ('Number of Note Ons that are preceded by isolated MIDI Pitch '
'Bend messages as a fraction of the total number of Note Ons.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Strongest Rhythmic Pulse'
self.description = 'Bin label of the beat bin with the highest frequency.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Second Strongest Rhythmic Pulse'
self.description = ('Bin label of the beat bin of the peak '
'with the second highest frequency.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Harmonicity of Two Strongest Rhythmic Pulses'
self.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.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Strength of Strongest Rhythmic Pulse'
self.description = 'Frequency of the beat bin with the highest frequency.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Strength of Second Strongest Rhythmic Pulse'
self.description = ('Frequency of the beat bin of the peak '
'with the second highest frequency.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Strength Ratio of Two Strongest Rhythmic Pulses'
self.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.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Combined Strength of Two Strongest Rhythmic Pulses'
self.description = ('The sum of the frequencies of the two beat bins '
'of the peaks with the highest frequencies.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Number of Strong Pulses'
self.description = 'Number of beat peaks with normalized frequencies over 0.1.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Number of Moderate Pulses'
self.description = 'Number of beat peaks with normalized frequencies over 0.01.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Number of Relatively Strong Pulses'
self.description = ('Number of beat peaks with frequencies at least 30% as high as '
'the frequency of the bin with the highest frequency.')
self.isSequential = True
self.dimensions = 1
[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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Rhythmic Looseness'
self.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.''')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
featuresModule.FeatureExtractor.__init__(self,
dataOrStream=dataOrStream,
**keywords)
self.name = 'Polyrhythms'
self.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.'''
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Rhythmic Variability'
self.description = 'Standard deviation of the bin values (except the first 40 empty ones).'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Beat Histogram'
self.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).')
self.isSequential = True
self.dimensions = 161
self.discrete = False
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Note Density'
self.description = 'Average number of notes per second.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Average Note Duration'
self.description = 'Average duration of notes in seconds.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Variability of Note Duration'
self.description = 'Standard deviation of note durations in seconds.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Maximum Note Duration'
self.description = 'Duration of the longest note (in seconds).'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Minimum Note Duration'
self.description = 'Duration of the shortest note (in seconds).'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Staccato Incidence'
self.description = ('Number of notes with durations of less than a 10th '
'of a second divided by the total number of notes in the recording.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Average Time Between Attacks'
self.description = 'Average time in seconds between Note On events (regardless of channel).'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Variability of Time Between Attacks'
self.description = ('Standard deviation of the times, in seconds, '
'between Note On events (regardless of channel).')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Average Time Between Attacks For Each Voice'
self.description = ('Average of average times in seconds between Note On events '
'on individual channels that contain at least one note.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Average Variability of Time Between Attacks For Each Voice'
self.description = ('Average standard deviation, in seconds, of time between '
'Note On events on individual channels that contain '
'at least one note.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Initial Tempo'
self.description = 'Tempo in beats per minute at the start of the recording.'
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Initial Time Signature'
self.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.')
self.isSequential = True
self.dimensions = 2
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Compound Or Simple Meter'
self.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).')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Triple Meter'
self.description = ('Set to 1 if numerator of initial time signature is 3, '
'set to 0 otherwise.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Quintuple Meter'
self.description = ('Set to 1 if numerator of initial time signature is 5, '
'set to 0 otherwise.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Changes of Meter'
self.description = ('Set to 1 if the time signature is changed one or more '
'times during the recording')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Duration'
self.description = 'The total duration in seconds of the music.'
self.isSequential = False # this is the only jSymbolic non seq feature
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Overall Dynamic Range'
self.description = 'The maximum loudness minus the minimum loudness value.'
self.isSequential = True
self.dimensions = 1
[docs]
class VariationOfDynamicsFeature(featuresModule.FeatureExtractor):
'''
Not implemented
Standard deviation of loudness levels of all notes.
TODO: implement
'''
id = 'D2'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Variation of Dynamics'
self.description = 'Standard deviation of loudness levels of all notes.'
self.isSequential = True
self.dimensions = 1
[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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Variation of Dynamics In Each Voice'
self.description = ('The average of the standard deviations of loudness '
'levels within each channel that contains at least one note.')
self.isSequential = True
self.dimensions = 1
[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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Average Note To Note Dynamics Change'
self.description = ('Average change of loudness from one note to the next note '
'in the same channel (in MIDI velocity units).')
self.isSequential = True
self.dimensions = 1
# ------------------------------------------------------------------------------
# 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Maximum Number of Independent Voices'
self.description = ('Maximum number of different channels in which notes '
'have sounded simultaneously. Here, Parts are treated as channels.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
# 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Average Number of Independent Voices'
self.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')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
# 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Variability of Number of Independent Voices'
self.description = ('Standard deviation of number of different channels '
'in which notes have sounded simultaneously. Rests are '
'not included in this calculation.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
# 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Voice Equality - Number of Notes'
self.description = ('Standard deviation of the total number of Note Ons '
'in each channel that contains at least one note.')
self.isSequential = True
self.dimensions = 1
[docs]
class VoiceEqualityNoteDurationFeature(featuresModule.FeatureExtractor):
'''
Not implemented
TODO: implement
'''
id = 'T5'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Voice Equality - Note Duration'
self.description = ('Standard deviation of the total duration of notes in seconds '
'in each channel that contains at least one note.')
self.isSequential = True
self.dimensions = 1
[docs]
class VoiceEqualityDynamicsFeature(featuresModule.FeatureExtractor):
'''
Not implemented
TODO: implement
'''
id = 'T6'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Voice Equality - Dynamics'
self.description = ('Standard deviation of the average volume of notes '
'in each channel that contains at least one note.')
self.isSequential = True
self.dimensions = 1
[docs]
class VoiceEqualityMelodicLeapsFeature(featuresModule.FeatureExtractor):
'''
Not implemented
TODO: implement
'''
id = 'T7'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Voice Equality - Melodic Leaps'
self.description = dedent('''
Standard deviation
of the average melodic leap in MIDI pitches
for each channel that contains at least one note.''')
self.isSequential = True
self.dimensions = 1
[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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Voice Equality - Range'
self.description = dedent('''
Standard deviation of the differences between the
highest and lowest pitches in each channel that contains at least one note.''')
self.isSequential = True
self.dimensions = 1
[docs]
class ImportanceOfLoudestVoiceFeature(featuresModule.FeatureExtractor):
'''
Not implemented
TODO: implement
'''
id = 'T9'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Importance of Loudest Voice'
self.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.''')
self.isSequential = True
self.dimensions = 1
[docs]
class RelativeRangeOfLoudestVoiceFeature(featuresModule.FeatureExtractor):
'''
Not implemented
TODO: implement
'''
id = 'T10'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Relative Range of Loudest Voice'
self.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.''')
self.isSequential = True
self.dimensions = 1
[docs]
class RangeOfHighestLineFeature(featuresModule.FeatureExtractor):
'''
Not implemented
TODO: implement
'''
id = 'T12'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Range of Highest Line'
self.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.''')
self.isSequential = True
self.dimensions = 1
[docs]
class RelativeNoteDensityOfHighestLineFeature(featuresModule.FeatureExtractor):
'''
Not implemented
TODO: implement
'''
id = 'T13'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Relative Note Density of Highest Line'
self.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.''')
self.isSequential = True
self.dimensions = 1
[docs]
class MelodicIntervalsInLowestLineFeature(featuresModule.FeatureExtractor):
'''
Not implemented
TODO: implement
'''
id = 'T15'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Melodic Intervals in Lowest Line'
self.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.''')
self.isSequential = True
self.dimensions = 1
[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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Voice Separation'
self.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.''')
self.isSequential = True
self.dimensions = 1
# ------------------------------------------------------------------------------
# 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Pitched Instruments Present'
self.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.''')
self.isSequential = True
self.dimensions = 128
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Unpitched Instruments Present'
self.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.''')
self.isSequential = True
self.dimensions = 47
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Note Prevalence of Pitched Instruments'
self.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.')
self.isSequential = True
self.dimensions = 128
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Note Prevalence of Unpitched Instruments'
self.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.''')
self.isSequential = True
self.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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Time Prevalence of Pitched Instruments'
self.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.')
self.isSequential = True
self.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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Variability of Note Prevalence of Pitched Instruments'
self.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.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Variability of Note Prevalence of Unpitched Instruments'
self.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.')
self.isSequential = True
self.dimensions = 1
[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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Number of Pitched Instruments'
self.description = ('Total number of General MIDI patches that are used to '
'play at least one note.')
self.isSequential = True
self.dimensions = 1
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Number of Unpitched Instruments'
self.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.')
self.isSequential = True
self.dimensions = 1
[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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Percussion Prevalence'
self.description = ('Total number of Note Ons corresponding to unpitched percussion '
'instruments divided by total number of Note Ons in the recording.')
self.isSequential = True
self.dimensions = 1
[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
'''
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
# subclasses must define
self._targetPrograms = []
[docs]
def process(self):
'''
Do processing necessary, storing result in 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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'String Keyboard Fraction'
self.description = ('Fraction of all Note Ons belonging to string keyboard patches '
'(General MIDI patches 1 to 8).')
self.isSequential = True
self.dimensions = 1
self._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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Acoustic Guitar Fraction'
self.description = ('Fraction of all Note Ons belonging to acoustic guitar patches '
'(General MIDI patches 25 and 26).')
self.isSequential = True
self.dimensions = 1
self._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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Electric Guitar Fraction'
self.description = ('Fraction of all Note Ons belonging to '
'electric guitar patches (General MIDI patches 27 to 32).')
self.isSequential = True
self.dimensions = 1
self._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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Violin Fraction'
self.description = ('Fraction of all Note Ons belonging to violin patches '
'(General MIDI patches 41 or 111).')
self.isSequential = True
self.dimensions = 1
self._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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Saxophone Fraction'
self.description = ('Fraction of all Note Ons belonging to saxophone patches '
'(General MIDI patches 65 through 68).')
self.isSequential = True
self.dimensions = 1
self._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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Brass Fraction'
self.description = ('Fraction of all Note Ons belonging to brass patches '
'(General MIDI patches 57 through 68).') # note: incorrect
self.isSequential = True
self.dimensions = 1
self._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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Woodwinds Fraction'
self.description = ('Fraction of all Note Ons belonging to woodwind patches '
'(General MIDI patches 69 through 76).')
self.isSequential = True
self.dimensions = 1
self._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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Orchestral Strings Fraction'
self.description = ('Fraction of all Note Ons belonging to orchestral strings patches '
'(General MIDI patches 41 or 47).')
self.isSequential = True
self.dimensions = 1
self._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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'String Ensemble Fraction'
self.description = ('Fraction of all Note Ons belonging to string ensemble patches '
'(General MIDI patches 49 to 52).')
self.isSequential = True
self.dimensions = 1
self._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'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Electric Instrument Fraction'
self.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).')
self.isSequential = True
self.dimensions = 1
self._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)