Source code for music21.analysis.floatingKey

# ------------------------------------------------------------------------------
# Name:         analysis/floatingKey.py
# Purpose:      Framework for floating key analysis
#
# Authors:      Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2015 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
The floatingKey analyzer will give an approximation of the key at any point in
a score down to the measure level using a fixed window.  It helps smooth out
measures emphasizing non-chord tones, etc.
'''
from __future__ import annotations

import copy

from music21 import key
from music21 import stream
from music21.exceptions21 import AnalysisException

class FloatingKeyException(AnalysisException):
    pass

[docs] class KeyAnalyzer: ''' KeyAnalyzer is the main object to use for floating analysis. The `windowSize` attribute (default 4) determines how many measures to look at in making the decision. Make it larger for pieces (like Mozart sonatas) that you expect fewer key changes. Make it smaller for pieces (like Bach chorales) that you expect more key changes. Or set it to an integer based on the number of the measures in the piece. The `weightAlgorithm` attribute determines how to scale the weight of measures according to their distance. Currently only one algorithm is supported: floatingKey.divide. TODO: Needs more work to work with second endings, partial measures, etc. >>> b = corpus.parse('bwv66.6') >>> ka = analysis.floatingKey.KeyAnalyzer(b) >>> ka.windowSize = 2 # chorale uses quick key changes >>> ka.run() # first measure is the pickup [<music21.key.Key of A major>, <music21.key.Key of A major>, <music21.key.Key of A major>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>] Raw analysis (no smoothing): >>> ka.getRawKeyByMeasure() [<music21.key.Key of A major>, <music21.key.Key of E major>, <music21.key.Key of A major>, <music21.key.Key of f# minor>, <music21.key.Key of E major>, <music21.key.Key of A major>, <music21.key.Key of b minor>, <music21.key.Key of C# major>, <music21.key.Key of F# major>, <music21.key.Key of b minor>] Major smoothing: >>> ka.windowSize = ka.numMeasures // 2 >>> ka.run() # only the pickup seems to be in A major by this approach [<music21.key.Key of A major>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>, <music21.key.Key of f# minor>] >>> tiny = converter.parse('tinyNotation: c1 e1 g1 c1 d-4 d-4 d-4 d-4') >>> ka = analysis.floatingKey.KeyAnalyzer(tiny) >>> ka.windowSize = 1 >>> ka.run() # This previously only gave four elements: am, CM, CM, CM [<music21.key.Key of a minor>, <music21.key.Key of C major>, <music21.key.Key of C major>, <music21.key.Key of C major>, <music21.key.Key of b- minor>] No measures will fail. >>> s = stream.Part([note.Note()]) >>> ka = analysis.floatingKey.KeyAnalyzer(s) Traceback (most recent call last): music21.analysis.floatingKey.FloatingKeyException: Stream must have Measures inside it * Changed in v7: analysis now incorporates final measures in pieces without pickup measures: ''' def __init__(self, s=None): if s is None: raise FloatingKeyException('Need a Stream to initialize') self.stream = s self.windowSize = 4 self.rawKeyByMeasure = [] self._interpretationMeasureDict = {} self.weightAlgorithm = divide if s.hasPartLikeStreams(): p = s.iter().parts.first() else: p = s # could be wrong for endings, etc. self.numMeasures = len(p.getElementsByClass(stream.Measure)) if self.numMeasures == 0: raise FloatingKeyException('Stream must have Measures inside it')
[docs] def run(self): self.getRawKeyByMeasure() return self.smoothInterpretationByMeasure()
[docs] def getRawKeyByMeasure(self): keyByMeasure = [] for i in range(self.numMeasures): # now `m` is a measure-slice of the entire stream m = self.stream.measure(i, indicesNotNumbers=True) if m is None or not m.recurse().notes: k = None else: k = m.analyze('key') keyByMeasure.append(k) self.rawKeyByMeasure = keyByMeasure return keyByMeasure
[docs] def getInterpretationByMeasure(self, mIndex): ''' Returns a dictionary of interpretations for the measure. `mIndex` is 0-indexed. ''' if mIndex in self._interpretationMeasureDict: return self._interpretationMeasureDict[mIndex] # CACHE if not self.rawKeyByMeasure: self.getRawKeyByMeasure() mk = self.rawKeyByMeasure[mIndex] if mk is None: return None # noinspection PyDictCreation interpretations = {} interpretations[mk.tonicPitchNameWithCase] = mk.correlationCoefficient for otherKey in mk.alternateInterpretations: interpretations[otherKey.tonicPitchNameWithCase] = otherKey.correlationCoefficient self._interpretationMeasureDict[mIndex] = interpretations return copy.copy(interpretations) # for manipulating
[docs] def smoothInterpretationByMeasure(self): smoothedKeysByMeasure = [] algorithm = self.weightAlgorithm for i in range(self.numMeasures): baseInterpretations = self.getInterpretationByMeasure(i) if baseInterpretations is None: continue for j in range(-1 * self.windowSize, self.windowSize + 1): # -2, -1, 0, 1, 2 etc. mNum = i + j if mNum < 0 or mNum >= self.numMeasures or mNum == i: continue newInterpretations = self.getInterpretationByMeasure(mNum) if newInterpretations is not None: for k in baseInterpretations: coefficient = algorithm(newInterpretations[k], j) baseInterpretations[k] += coefficient bestName = max(baseInterpretations, key=baseInterpretations.get) smoothedKeysByMeasure.append(key.Key(bestName)) return smoothedKeysByMeasure
[docs] def divide(coefficient, distance): ''' Divide the coefficient by the absolute value of the distance + 1 >>> analysis.floatingKey.divide(4.0, -1) 2.0 ''' return coefficient / (abs(distance) + 1)
if __name__ == '__main__': import music21 music21.mainTest()