# ------------------------------------------------------------------------------
# Name: features.native.py
# Purpose: music21 feature extractors
#
# Authors: Christopher Ariza
#
# Copyright: Copyright © 2011 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Original music21 feature extractors.
'''
from __future__ import annotations
import unittest
from music21 import environment
from music21.features import base as featuresModule
from music21 import text
environLocal = environment.Environment('features.native')
# ------------------------------------------------------------------------------
# ideas for other music21 features extractors
# notation features: clef usage, enharmonic usage
# chromatic alteration related to beat position
# key signature histogram
# array of circle of fifths
# lyrics
# Luca Gloria:
# searching for numbers of hits
# vowel metrical position
# idea of language/text specific -- DONE
# Essen locale and elevation
# automatic key analysis
# as a method of feature extraction
# key detection on windowed segments
# prevalence m/M over 4 bar windows
# key ambiguity list
# correlation coefficient
# harmony realization also adds pitches not available in midi
# ------------------------------------------------------------------------------
class NativeFeatureException(featuresModule.FeatureException):
pass
[docs]
class QualityFeature(featuresModule.FeatureExtractor):
'''
Extends the jSymbolic QualityFeature to automatically find mode.
Set to 0 if the key signature indicates that
a recording is major, set to 1 if it indicates
that it is minor. A Music21
addition: if no key mode is found in the piece, or conflicting modes in the keys,
analyze the piece to discover what mode it is most likely in.
Example: Handel, Rinaldo Aria (musicxml) is explicitly encoded as being in Major:
>>> s = corpus.parse('handel/rinaldo/lascia_chio_pianga')
>>> fe = features.native.QualityFeature(s)
>>> f = fe.extract()
>>> f.vector
[0]
now we will try it with the last movement of Schoenberg's opus 19 which has
no mode explicitly encoded in the musicxml but which our analysis routines
believe (having very little to go on) fits the profile of e minor best.
>>> schoenberg19mvmt6 = corpus.parse('schoenberg/opus19', 6)
>>> fe2 = features.native.QualityFeature(schoenberg19mvmt6)
>>> f2 = fe2.extract()
>>> f2.vector
[1]
OMIT_FROM_DOCS
# for monophonic melodies
# incomplete measures / pickups for monophonic melodies
'''
id = 'P22'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Quality'
self.description = '''
Set to 0 if the Key or KeySignature indicates that
a recording is major, set to 1 if it indicates
that it is minor.
Music21 addition: if no key mode is found in the piece, or conflicting
modes in the keys, analyze the piece to
discover what mode it is most likely in.
'''
self.isSequential = True
self.dimensions = 1
[docs]
def process(self) -> None:
'''
Do processing necessary, storing result in feature.
'''
if self.data is None or self.feature is None: # pragma: no cover
raise ValueError('Cannot process without a data instance or feature.')
allKeys = self.data['flat.getElementsByClass(Key)']
keyFeature: int|None = None
if len(allKeys) == 1:
k0 = allKeys[0]
if k0.mode == 'major':
keyFeature = 0
elif k0.mode == 'minor':
keyFeature = 1
self.feature.vector[0] = keyFeature
return
useKey = None
if len(allKeys) == 1:
useKey = allKeys[0]
elif len(allKeys) > 1:
seen_modes = set()
for k in allKeys:
seen_modes.add(k.mode)
if len(seen_modes) == 1:
# there might, for instance be lots of different parts
# all giving the same mode. (maybe not the same key
# because of transposition). It doesn't matter which
# key we use for this.
useKey = allKeys[0]
# else -- back to analysis.
if useKey is None:
useKey = self.data['flat.analyzedKey']
analyzedMode = useKey.mode
if analyzedMode == 'major':
keyFeature = 0
elif analyzedMode == 'minor':
keyFeature = 1
else:
raise NativeFeatureException(
'should be able to get a mode from something here -- '
+ 'perhaps there are no notes?'
)
self.feature.vector[0] = keyFeature
# ------------------------------------------------------------------------------
[docs]
class TonalCertainty(featuresModule.FeatureExtractor):
'''
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.TonalCertainty(s)
>>> f = fe.extract()
>>> f.vector
[1.26...]
>>> pitches = [56, 55, 56, 57, 58, 57, 58, 59, 60, 59, 60, 61, 62, 61,
... 62, 63, 64, 63, 64, 65, 66, 65, 66, 67]
>>> s = stream.Stream()
>>> for pitch in pitches:
... s.append(note.Note(pitch))
>>> features.native.TonalCertainty(s).extract().vector
[0.0]
'''
id = 'K1' # TODO: need id
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Tonal Certainty'
self.description = ('A floating point magnitude value that suggest tonal '
'certainty based on automatic key analysis.')
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
self.feature.vector[0] = self.data['flat.analyzedKey.tonalCertainty']
# ------------------------------------------------------------------------------
# features that use metrical distinctions
[docs]
class FirstBeatAttackPrevalence(featuresModule.FeatureExtractor):
'''
NOT IMPLEMENTED!
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.FirstBeatAttackPrevalence(s)
>>> f = fe.extract()
>>> f.vector
[0]
TODO: Implement!
'''
id = 'MP1'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'First Beat Attack Prevalence'
self.description = ('Fraction of first beats of a measure that have notes '
'that start on this beat.')
self.dimensions = 1
self.discrete = False
# ------------------------------------------------------------------------------
# employing symbolic durations
[docs]
class UniqueNoteQuarterLengths(featuresModule.FeatureExtractor):
'''
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.UniqueNoteQuarterLengths(s)
>>> fe.extract().vector
[3]
'''
id = 'QL1'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Unique Note Quarter Lengths'
self.description = 'The number of unique note quarter lengths.'
self.dimensions = 1
self.discrete = True
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
count = 0
histo = self.data['flat.notes.quarterLengthHistogram']
for key in histo:
# all defined keys should be greater than zero, but just in case
if histo[key] > 0:
count += 1
self.feature.vector[0] = count
[docs]
class MostCommonNoteQuarterLength(featuresModule.FeatureExtractor):
'''
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.MostCommonNoteQuarterLength(s)
>>> fe.extract().vector
[1.0]
'''
id = 'QL2'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Note Quarter Length'
self.description = 'The value of the most common quarter length.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
histo = self.data['flat.notes.quarterLengthHistogram']
maximum = 0
ql = 0
for key in histo:
# all defined keys should be greater than zero, but just in case
if histo[key] >= maximum:
maximum = histo[key]
ql = key
self.feature.vector[0] = ql
[docs]
class MostCommonNoteQuarterLengthPrevalence(featuresModule.FeatureExtractor):
'''
Fraction of notes that have the most common quarter length.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.MostCommonNoteQuarterLengthPrevalence(s)
>>> fe.extract().vector
[0.60...]
'''
id = 'QL3'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Note Quarter Length Prevalence'
self.description = 'Fraction of notes that have the most common quarter length.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
summation = 0 # count of all
histo = self.data['flat.notes.quarterLengthHistogram']
if not histo:
raise NativeFeatureException('input lacks notes')
maxKey = 0 # max found for any one key
for key in histo:
# all defined keys should be greater than zero, but just in case
if histo[key] > 0:
summation += histo[key]
if histo[key] >= maxKey:
maxKey = histo[key]
self.feature.vector[0] = maxKey / summation
[docs]
class RangeOfNoteQuarterLengths(featuresModule.FeatureExtractor):
'''
Difference between the longest and shortest quarter lengths.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.RangeOfNoteQuarterLengths(s)
>>> fe.extract().vector
[1.5]
'''
id = 'QL4'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Range of Note Quarter Lengths'
self.description = 'Difference between the longest and shortest quarter lengths.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
histo = self.data['flat.notes.quarterLengthHistogram']
if not histo:
raise NativeFeatureException('input lacks notes')
minVal = min(histo.keys())
maxVal = max(histo.keys())
self.feature.vector[0] = maxVal - minVal
# ------------------------------------------------------------------------------
# various ways of looking at chordify representation
# percentage of closed-position chords and
# percentage of closed-position chords above bass -- which looks at how many
# 2 (or 3 in the second one) note chordify simultaneities are the same after
# running .closedPosition() on them. For the latter, we just delete the
# lowest note of the chord before running that.
[docs]
class UniquePitchClassSetSimultaneities(featuresModule.FeatureExtractor):
'''
Number of unique pitch class simultaneities.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.UniquePitchClassSetSimultaneities(s)
>>> fe.extract().vector
[27]
'''
id = 'CS1'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Unique Pitch Class Set Simultaneities'
self.description = 'Number of unique pitch class simultaneities.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
count = 0
histo = self.data['chordify.flat.getElementsByClass(Chord).pitchClassSetHistogram']
for key in histo:
# all defined keys should be greater than zero, but just in case
if histo[key] > 0:
count += 1
self.feature.vector[0] = count
[docs]
class UniqueSetClassSimultaneities(featuresModule.FeatureExtractor):
'''
Number of unique set class simultaneities.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.UniqueSetClassSimultaneities(s)
>>> fe.extract().vector
[14]
'''
id = 'CS2'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Unique Set Class Simultaneities'
self.description = 'Number of unique set class simultaneities.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
count = 0
histo = self.data['chordify.flat.getElementsByClass(Chord).setClassHistogram']
for key in histo:
# all defined keys should be greater than zero, but just in case
if histo[key] > 0:
count += 1
self.feature.vector[0] = count
[docs]
class MostCommonPitchClassSetSimultaneityPrevalence(
featuresModule.FeatureExtractor):
'''
Fraction of all pitch class simultaneities that are the most common simultaneity.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.MostCommonPitchClassSetSimultaneityPrevalence(s)
>>> fe.extract().vector
[0.134...]
'''
id = 'CS3'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Pitch Class Set Simultaneity Prevalence'
self.description = ('Fraction of all pitch class simultaneities that are '
'the most common simultaneity.')
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
summation = 0 # count of all
histo = self.data['chordify.flat.getElementsByClass(Chord).pitchClassSetHistogram']
maxKey = 0 # max found for any one key
if not histo:
raise NativeFeatureException('input lacks notes')
for key in histo:
# all defined keys should be greater than zero, but just in case
if histo[key] > 0:
summation += histo[key]
if histo[key] >= maxKey:
maxKey = histo[key]
if summation != 0:
self.feature.vector[0] = maxKey / summation
else:
self.feature.vector[0] = 0
[docs]
class MostCommonSetClassSimultaneityPrevalence(featuresModule.FeatureExtractor):
'''
Fraction of all set class simultaneities that the most common simultaneity
occupies.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.MostCommonSetClassSimultaneityPrevalence(s)
>>> fe.extract().vector
[0.653...]
>>> s2 = corpus.parse('schoenberg/opus19', 6)
>>> fe2 = features.native.MostCommonSetClassSimultaneityPrevalence(s2)
>>> fe2.extract().vector
[0.235...]
'''
id = 'CS4'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Most Common Set Class Simultaneity Prevalence'
self.description = ('Fraction of all set class simultaneities that '
'are the most common simultaneity.')
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
summation = 0 # count of all
histo = self.data['chordify.flat.getElementsByClass(Chord).setClassHistogram']
if not histo:
raise NativeFeatureException('input lacks notes')
maxKey = 0 # max found for any one key
for key in histo:
# all defined keys should be greater than zero, but just in case
if histo[key] > 0:
summation += histo[key]
if histo[key] >= maxKey:
maxKey = histo[key]
if summation != 0:
self.feature.vector[0] = maxKey / summation
else:
self.feature.vector[0] = 0
[docs]
class MajorTriadSimultaneityPrevalence(featuresModule.FeatureExtractor):
'''
Percentage of all simultaneities that are major triads.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.MajorTriadSimultaneityPrevalence(s)
>>> fe.extract().vector
[0.46...]
'''
id = 'CS5'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Major Triad Simultaneity Prevalence'
self.description = 'Percentage of all simultaneities that are major triads.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
# use for total number of chords
total = len(self.data['chordify.flat.getElementsByClass(Chord)'])
histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram']
# using incomplete
if total != 0:
part = histo['isMajorTriad'] + histo['isIncompleteMajorTriad']
self.feature.vector[0] = part / total
else:
self.feature.vector[0] = 0
[docs]
class MinorTriadSimultaneityPrevalence(featuresModule.FeatureExtractor):
'''
Percentage of all simultaneities that are minor triads.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.MinorTriadSimultaneityPrevalence(s)
>>> fe.extract().vector # same as major in this work
[0.211...]
'''
id = 'CS6'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Minor Triad Simultaneity Prevalence'
self.description = 'Percentage of all simultaneities that are minor triads.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
# use for total number of chords
total = len(self.data['chordify.flat.getElementsByClass(Chord)'])
histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram']
# using incomplete
if total != 0:
part = histo['isMinorTriad'] + histo['isIncompleteMinorTriad']
self.feature.vector[0] = part / total
else:
self.feature.vector[0] = 0
[docs]
class DominantSeventhSimultaneityPrevalence(featuresModule.FeatureExtractor):
'''
Percentage of all simultaneities that are dominant seventh.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.DominantSeventhSimultaneityPrevalence(s)
>>> fe.extract().vector
[0.076...]
'''
id = 'CS7'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Dominant Seventh Simultaneity Prevalence'
self.description = 'Percentage of all simultaneities that are dominant seventh.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
# use for total number of chords
total = len(self.data['chordify.flat.getElementsByClass(Chord)'])
histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram']
# using incomplete
if total != 0:
part = histo['isDominantSeventh']
self.feature.vector[0] = part / total
else:
self.feature.vector[0] = 0
[docs]
class DiminishedTriadSimultaneityPrevalence(featuresModule.FeatureExtractor):
'''
Percentage of all simultaneities that are diminished triads.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.DiminishedTriadSimultaneityPrevalence(s)
>>> fe.extract().vector
[0.019...]
'''
id = 'CS8'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Diminished Triad Simultaneity Prevalence'
self.description = 'Percentage of all simultaneities that are diminished triads.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
# use for total number of chords
total = len(self.data['chordify.flat.getElementsByClass(Chord)'])
histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram']
# using incomplete
if total != 0:
part = histo['isDiminishedTriad']
self.feature.vector[0] = part / total
else:
self.feature.vector[0] = 0
[docs]
class TriadSimultaneityPrevalence(featuresModule.FeatureExtractor):
'''
Gives the proportion of all simultaneities which form triads (major,
minor, diminished, or augmented)
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.TriadSimultaneityPrevalence(s)
>>> fe.extract().vector
[0.692...]
>>> s2 = corpus.parse('schoenberg/opus19', 2)
>>> fe2 = features.native.TriadSimultaneityPrevalence(s2)
>>> fe2.extract().vector
[0.02272727...]
'''
id = 'CS9'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Triad Simultaneity Prevalence'
self.description = 'Proportion of all simultaneities that form triads.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
# use for total number of chords
total = len(self.data['chordify.flat.getElementsByClass(Chord)'])
histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram']
# using incomplete
if total != 0:
part = histo['isTriad']
self.feature.vector[0] = part / total
else:
self.feature.vector[0] = 0
[docs]
class DiminishedSeventhSimultaneityPrevalence(featuresModule.FeatureExtractor):
'''
Percentage of all simultaneities that are diminished seventh chords.
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.DiminishedSeventhSimultaneityPrevalence(s)
>>> fe.extract().vector
[0.0]
'''
id = 'CS10'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Diminished Seventh Simultaneity Prevalence'
self.description = 'Percentage of all simultaneities that are diminished seventh chords.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
# use for total number of chords
total = len(self.data['chordify.flat.getElementsByClass(Chord)'])
histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram']
# using incomplete
if total != 0:
part = histo['isDiminishedSeventh']
self.feature.vector[0] = part / total
else:
self.feature.vector[0] = 0
[docs]
class IncorrectlySpelledTriadPrevalence(featuresModule.FeatureExtractor):
'''
Percentage of all triads that are spelled incorrectly.
example:
Mozart k155 movement 2 has a single instance of an incorrectly spelled
triad (m. 17, where the C# of an A-major chord has a lower neighbor B#
thus temporarily creating an incorrectly spelled A-minor chord).
We would expect highly chromatic music such as Reger or Wagner to have
a higher percentage, or automatically rendered MIDI
transcriptions (which don't distinguish between D# and Eb).
>>> s = corpus.parse('bwv66.6')
>>> fe = features.native.IncorrectlySpelledTriadPrevalence(s)
>>> fe.extract().vector
[0.02...]
'''
id = 'CS11'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Incorrectly Spelled Triad Prevalence'
self.description = 'Percentage of all triads that are spelled incorrectly.'
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
# use for total number of chords
histo = self.data['chordify.flat.getElementsByClass(Chord).typesHistogram']
if not histo:
raise NativeFeatureException('input lacks notes')
# using incomplete
totalCorrectlySpelled = histo['isTriad']
forteData = self.data['chordify.flat.getElementsByClass(Chord).setClassHistogram']
totalForteTriads = 0
if '3-11' in forteData:
totalForteTriads += forteData['3-11']
if '3-12' in forteData:
totalForteTriads += forteData['3-12']
if '3-10' in forteData:
totalForteTriads += forteData['3-10']
totalIncorrectlySpelled = totalForteTriads - totalCorrectlySpelled
if totalForteTriads != 0:
self.feature.vector[0] = totalIncorrectlySpelled / totalForteTriads
else:
raise NativeFeatureException('input lacks Forte triads')
[docs]
class ChordBassMotionFeature(featuresModule.FeatureExtractor):
'''
A twelve element feature that reports the fraction
of all chord motion of music21.harmony.Harmony objects
that move up by i-half-steps. (a half-step motion down would
be stored in i = 11). i = 0 is always 0.0 since consecutive
chords on the same pitch are ignored (unless there are 0 or 1 harmonies, in which case it is 1)
Sample test on Dylan's Blowing In The Wind (not included), showing all
motion is 3rds, 6ths, or especially 4ths and 5ths.
s = corpus.parse('demos/BlowinInTheWind')
fe = features.native.ChordBassMotionFeature(s)
fe.extract().vector
[0.0, 0.0, 0.0, 0.0416..., 0.0416..., 0.166..., 0.0, 0.54166..., 0.0, 0.0, 0.2083... 0.0]
For comparison, the Beatles Here Comes the Sun has more tone motion
[0.0, 0.05..., 0.14..., 0.03..., 0.06..., 0.3..., 0.008..., 0.303...,
0.0, 0.0, 0.07..., 0.008...]
Post 1990s music has a lot more semitone motion.
'''
id = 'CS12'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Chord Bass Motion'
self.description = ('12-element vector showing the fraction of chords that move '
'by x semitones (where x=0 is always 0 unless there are 0 '
'or 1 harmonies, in which case it is 1).')
self.dimensions = 12
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
# use for total number of chords
harms = self.data['flat.getElementsByClass(Harmony)']
totMotion = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
totalHarmonicMotion = 0
lastHarm = None
for thisHarm in harms:
if lastHarm is None:
lastHarm = thisHarm
else:
if lastHarm.bass() is not None:
lastBass = lastHarm.bass()
else:
lastBass = lastHarm.root()
if thisHarm.bass() is not None:
thisBass = thisHarm.bass()
else:
thisBass = thisHarm.root()
if lastBass.pitchClass == thisBass.pitchClass:
pass
else:
halfStepMotion = (lastBass.pitchClass - thisBass.pitchClass) % 12
totMotion[halfStepMotion] += 1
totalHarmonicMotion += 1
lastHarm = thisHarm
if totalHarmonicMotion == 0:
vector = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
else:
totHarmonicMotionFraction = [0.0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
for i in range(1, 12):
totHarmonicMotionFraction[i] = float(totMotion[i]) / totalHarmonicMotion
vector = totHarmonicMotionFraction
self.feature.vector = vector
# ------------------------------------------------------------------------------
# metadata
[docs]
class ComposerPopularity(featuresModule.FeatureExtractor):
'''
REMOVED in v7 because Google's repsonse no longer includes result counts.
Empty class still here so that id won't be reused, but it's been removed
from this module's list of features.
'''
id = 'MD1'
# ------------------------------------------------------------------------------
# melodic contour
[docs]
class LandiniCadence(featuresModule.FeatureExtractor):
'''
Return a boolean if one or more Parts end with a Landini-like cadential figure.
'''
id = 'MC1'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Ends With Landini Melodic Contour'
self.description = ('Boolean that indicates the presence of a Landini-like '
'cadential figure in one or more parts.')
self.dimensions = 1
self.discrete = False
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
# store plausible ending half step movements
# these need to be lists for comparison
match = [[-2, 3], [-1, -2, 3]]
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)
# iterate over each contour
found = False
for cList in cBundle:
# remove repeated notes
cListClean = []
for c in cList:
if c != 0:
cListClean.append(c)
# find matches
for cMatch in match:
# environLocal.printDebug(['cList', cList, 'cListClean',
# cListClean, 'cMatch', cMatch])
# compare to last
if len(cListClean) >= len(cMatch):
# get the len of the last elements
if cListClean[-len(cMatch):] == cMatch:
found = True
break
if found:
break
if found:
self.feature.vector[0] = 1
# -----------------------------------------------------------------------------
# text features
[docs]
class LanguageFeature(featuresModule.FeatureExtractor):
'''
language of text as a number
the number is the index of text.LanguageDetector.languageCodes + 1
or 0 if there is no language.
Detect that the language of a Handel aria is Italian.
>>> s = corpus.parse('handel/rinaldo/lascia_chio_pianga')
>>> fe = features.native.LanguageFeature(s)
>>> fe.extract().vector
[3]
'''
id = 'TX1'
def __init__(self, dataOrStream=None, **keywords):
super().__init__(dataOrStream=dataOrStream, **keywords)
self.name = 'Language Feature'
self.description = ('Language of the lyrics of the piece given as a numeric '
'value from text.LanguageDetector.mostLikelyLanguageNumeric().')
self.dimensions = 1
self.discrete = True
self.languageDetector = text.LanguageDetector()
[docs]
def process(self):
'''
Do processing necessary, storing result in feature.
'''
storedLyrics = self.data['assembledLyrics']
self.feature.vector[0] = self.languageDetector.mostLikelyLanguageNumeric(storedLyrics)
# ------------------------------------------------------------------------------
featureExtractors = [
QualityFeature, # p22
TonalCertainty, # k1
UniqueNoteQuarterLengths, # ql1
MostCommonNoteQuarterLength, # ql2
MostCommonNoteQuarterLengthPrevalence, # ql3
RangeOfNoteQuarterLengths, # ql4
UniquePitchClassSetSimultaneities, # cs1
UniqueSetClassSimultaneities, # cs2
MostCommonPitchClassSetSimultaneityPrevalence, # cs3
MostCommonSetClassSimultaneityPrevalence, # cs4
MajorTriadSimultaneityPrevalence, # cs5
MinorTriadSimultaneityPrevalence, # cs6
DominantSeventhSimultaneityPrevalence, # cs7
DiminishedTriadSimultaneityPrevalence, # cs8
TriadSimultaneityPrevalence, # cs9
DiminishedSeventhSimultaneityPrevalence, # cs10
IncorrectlySpelledTriadPrevalence, # cs11
ChordBassMotionFeature, # cs12
# ComposerPopularity, # md1
LandiniCadence, # mc1
LanguageFeature, # tx1
]
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
def testIncorrectlySpelledTriadPrevalence(self):
from music21 import stream
from music21 import features
from music21 import chord
s = stream.Stream()
s.append(chord.Chord(['c', 'e', 'g']))
s.append(chord.Chord(['c', 'e', 'a']))
s.append(chord.Chord(['c', 'd#', 'g']))
s.append(chord.Chord(['c', 'd#', 'a--']))
fe = features.native.IncorrectlySpelledTriadPrevalence(s)
self.assertEqual(str(fe.extract().vector[0]), '0.5')
def testLandiniCadence(self):
from music21 import converter
from music21 import features
s = converter.parse('tinynotation: 3/4 f#4 f# e g2')
fe = features.native.LandiniCadence(s)
self.assertEqual(fe.extract().vector[0], 1)
s = converter.parse('tinynotation: 3/4 f#4 f# f# g2')
fe = features.native.LandiniCadence(s)
self.assertEqual(fe.extract().vector[0], 0)
s = converter.parse('tinynotation: 3/4 f#4 e a g2')
fe = features.native.LandiniCadence(s)
self.assertEqual(fe.extract().vector[0], 0)
if __name__ == '__main__':
import music21
music21.mainTest(Test)