Source code for music21.analysis.reduceChords

# -----------------------------------------------------------------------------
# Name:         reduceChords.py
# Purpose:      Tools for eliminating passing chords, etc.
#
# Authors:      Joséphine Wolf Oberholtzer
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2013 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# -----------------------------------------------------------------------------
'''
Automatically reduce a MeasureStack to a single chord or group of chords.
'''
from __future__ import annotations

import collections
from collections.abc import Sequence
import itertools
import unittest

from music21 import chord
from music21.common.types import DocOrder
from music21 import exceptions21
from music21 import environment
from music21 import meter
from music21 import note
from music21 import pitch
from music21 import stream
from music21 import tree

# from music21 import tie
environLocal = environment.Environment('reduceChords')

# -----------------------------------------------------------------------------

[docs] def testMeasureStream1(): ''' returns a simple measure stream for testing: >>> s = analysis.reduceChords.testMeasureStream1() >>> s.show('text') {0.0} <music21.meter.TimeSignature 4/4> {0.0} <music21.chord.Chord C4 E4 G4 C5> {2.0} <music21.chord.Chord C4 E4 F4 B4> {3.0} <music21.chord.Chord C4 E4 G4 C5> ''' # from music21 import chord measure = stream.Measure() timeSignature = meter.TimeSignature('4/4') chord1 = chord.Chord('C4 E4 G4 C5') chord1.quarterLength = 2.0 chord2 = chord.Chord('C4 E4 F4 B4') chord3 = chord.Chord('C4 E4 G4 C5') for element in (timeSignature, chord1, chord2, chord3): measure.append(element) return measure
# ----------------------------------------------------------------------------- class ChordReducerException(exceptions21.Music21Exception): pass
[docs] class ChordReducer: r''' A chord reducer. ''' def __init__(self): self.weightAlgorithm = self.qlbsmpConsonance self.maxChords = 3 self.positionInMeasure = None self.numberOfElementsInMeasure = None
[docs] def run(self, inputScore, allowableChords=None, closedPosition=False, forbiddenChords=None, maximumNumberOfChords=3): if not isinstance(inputScore, stream.Score): raise ChordReducerException('Must be called on a stream.Score') if allowableChords is not None: if not all(isinstance(x, chord.Chord) for x in allowableChords): raise ChordReducerException('All allowableChords must be Chords') intervalClassSets = [] for x in allowableChords: intervalClassSet = self._getIntervalClassSet(x.pitches) intervalClassSets.append(intervalClassSet) allowableChords = frozenset(intervalClassSets) if forbiddenChords is not None: if not all(isinstance(x, chord.Chord) for x in forbiddenChords): raise ChordReducerException('All forbiddenChords must be Chords') intervalClassSets = [] for x in allowableChords: intervalClassSet = self._getIntervalClassSet(x.pitches) intervalClassSets.append(intervalClassSet) forbiddenChords = frozenset(intervalClassSets) scoreTree = tree.fromStream.asTimespans( inputScore, flatten=True, classList=(note.Note, chord.Chord)) self.removeZeroDurationTimespans(scoreTree) self.splitByBass(scoreTree) self.removeVerticalDissonances(scoreTree=scoreTree, allowableChords=allowableChords, forbiddenChords=forbiddenChords) partwiseTrees = scoreTree.toPartwiseTimespanTrees() self.fillBassGaps(scoreTree, partwiseTrees) self.removeShortTimespans(scoreTree, partwiseTrees, duration=0.5) self.fillBassGaps(scoreTree, partwiseTrees) self.fillMeasureGaps(scoreTree, partwiseTrees) self.removeShortTimespans(scoreTree, partwiseTrees, duration=1.0) self.fillBassGaps(scoreTree, partwiseTrees) self.fillMeasureGaps(scoreTree, partwiseTrees) reduction = stream.Score() # partwiseReduction = tree.toPartwiseScore() # for part in partwiseReduction: # reduction.append(part) # TODO: make chordified a method on TimespanTree and move stream.chordify guts there # then use that and remove this deprecated call. import warnings with warnings.catch_warnings(): # catch deprecation warning warnings.simplefilter('ignore', category=exceptions21.Music21DeprecationWarning) chordifiedReduction = tree.toStream.chordified( scoreTree, templateStream=inputScore, ) chordifiedPart = stream.Part() for measure in chordifiedReduction.getElementsByClass(stream.Measure): reducedMeasure = self.reduceMeasureToNChords( measure, maximumNumberOfChords=maximumNumberOfChords, weightAlgorithm=self.qlbsmpConsonance, trimBelow=0.25, ) chordifiedPart.append(reducedMeasure) reduction.append(chordifiedPart) if closedPosition: for x in reduction[chord.Chord]: x.closedPosition(forceOctave=4, inPlace=True) return reduction
# PRIVATE METHODS # @staticmethod def _debug(scoreTree): for part, subtree in scoreTree.toPartwiseTimespanTrees().items(): print(part) for timespan in subtree: print('\t', timespan) overlap = subtree.maximumOverlap() if overlap >= 1: print(part) raise exceptions21.Music21Exception( 'maximumOverlap is exceeded' ) @staticmethod def _getIntervalClassSet(pitches_in: Sequence[pitch.Pitch|str|int]) -> frozenset[int]: ''' Return a frozenset of all the interval classes (1-6) in the given Sequence (list, tuple, etc.) of pitches or things like strings/ints that can be passed to the Pitch constructor. ''' result: set[int] = set() pitches: list[pitch.Pitch] = [] for p in pitches_in: if isinstance(p, pitch.Pitch): pitches.append(p) else: pitches.append(pitch.Pitch(p)) interval_int: int for i, x in enumerate(pitches): for y in pitches[i + 1:]: interval_int = int(abs(x.ps - y.ps)) interval_int %= 12 if interval_int >= 6: interval_int = 12 - interval_int result.add(interval_int) if 0 in result: result.remove(0) return frozenset(result) def _iterateElementsPairwise(self, inputStream): elementBuffer = collections.deque() prototype = ( chord.Chord, note.Note, note.Rest, ) for element in inputStream.flatten(): if not isinstance(element, prototype): continue elementBuffer.append(element) if len(elementBuffer) == 2: yield tuple(elementBuffer) elementBuffer.popleft() # PUBLIC METHODS #
[docs] def alignHockets(self, scoreTree): r''' Aligns hockets between parts in `tree`. ''' for verticalities in scoreTree.iterateVerticalitiesNwise(n=2): verticalityOne, verticalityTwo = verticalities pitchSetOne = verticalityOne.pitchSet pitchSetTwo = verticalityTwo.pitchSet if (not verticalityOne.isConsonant or not verticalityTwo.isConsonant): continue if verticalityOne.measureNumber != verticalityTwo.measureNumber: continue if verticalityOne.pitchSet == verticalityTwo.pitchSet: continue if pitchSetOne.issubset(pitchSetTwo): for timespan in verticalityTwo.startTimespans: scoreTree.removeTimespan(timespan) newTimespan = timespan.new( offset=verticalityOne.offset, ) newTimespan.beatStrength = verticalityOne.beatStrength scoreTree.insert(newTimespan) elif pitchSetTwo.issubset(pitchSetOne): for timespan in verticalityOne.startTimespans: if timespan.endTime < verticalityTwo.offset: scoreTree.removeTimespan(timespan) newTimespan = timespan.new( endTime=verticalityTwo.offset, ) scoreTree.insert(newTimespan)
[docs] def collapseArpeggios(self, scoreTree): r''' Collapses arpeggios in `tree`. >>> m = stream.Measure([chord.Chord('C4 E4'), chord.Chord('C4 G4')]) >>> cr = analysis.reduceChords.ChordReducer() >>> spans = m.asTimespans(classList=(note.NotRest,)) >>> len(spans) 2 >>> cr.collapseArpeggios(spans) >>> len(spans) 1 OMIT_FROM_DOCS Ensure it doesn't crash without a class filter: >>> s = corpus.parse('beach') >>> cr2 = analysis.reduceChords.ChordReducer() >>> excerpt_tree = s.parts.first().asTimespans() >>> cr2.collapseArpeggios(excerpt_tree) >>> excerpt_tree <TimespanTree {163} (0.0 to 124.0) <music21.stream.Part Soprano I>> ''' for verticalities in scoreTree.iterateVerticalitiesNwise(n=2): one, two = verticalities if not one.pitchSet or not two.pitchSet: continue onePitches = sorted(one.pitchSet) twoPitches = sorted(two.pitchSet) if onePitches[0].nameWithOctave != twoPitches[0].nameWithOctave: continue elif one.measureNumber != two.measureNumber: continue # # is this used? # bothPitches = set() # bothPitches.update([x.nameWithOctave for x in onePitches]) # bothPitches.update([x.nameWithOctave for x in twoPitches]) # bothPitches = sorted([pitch.Pitch(x) for x in bothPitches]) # if not timespanStream.Verticality.pitchesAreConsonant(bothPitches): # intervalClasses = self._getIntervalClassSet(bothPitches) # if intervalClasses not in ( # frozenset([1, 3, 4]), # frozenset([1, 4, 5]), # frozenset([2, 3, 5]), # frozenset([2, 4, 6]), # ): # continue horizontalities = scoreTree.unwrapVerticalities(verticalities) for unused_part, timespanList in horizontalities.items(): if len(timespanList) < 2: continue elif not all(isinstance(x, tree.spans.PitchedTimespan) for x in timespanList): continue elif timespanList[0].pitches == timespanList[1].pitches: continue bothPitches = timespanList[0].pitches + timespanList[1].pitches sumChord = chord.Chord(bothPitches) scoreTree.removeTimespanList(timespanList) merged = timespanList[0].new( element=sumChord, endTime=timespanList[1].endTime, ) scoreTree.insert(merged)
[docs] def computeMeasureChordWeights( self, measureObject, weightAlgorithm=None, ): ''' Compute measure chord weights: >>> s = analysis.reduceChords.testMeasureStream1().notes >>> cr = analysis.reduceChords.ChordReducer() >>> cws = cr.computeMeasureChordWeights(s) >>> for pcs in sorted(cws): ... print(f'{pcs!r:18} {cws[pcs]:2.1f}') (0, 4, 7) 3.0 (0, 11, 4, 5) 1.0 Add beatStrength: >>> cws = cr.computeMeasureChordWeights(s, ... weightAlgorithm=cr.quarterLengthBeatStrength) >>> for pcs in sorted(cws): ... print(f'{pcs!r:18} {cws[pcs]:2.1f}') (0, 4, 7) 2.2 (0, 11, 4, 5) 0.5 Give extra weight to the last element in a measure: >>> cws = cr.computeMeasureChordWeights(s, ... weightAlgorithm=cr.quarterLengthBeatStrengthMeasurePosition) >>> for pcs in sorted(cws): ... print(f'{pcs!r:18} {cws[pcs]:2.1f}') (0, 4, 7) 3.0 (0, 11, 4, 5) 0.5 Make consonance count a lot: >>> cws = cr.computeMeasureChordWeights(s, ... weightAlgorithm=cr.qlbsmpConsonance) >>> for pcs in sorted(cws): ... print(f'{pcs!r:18} {cws[pcs]:2.1f}') (0, 4, 7) 3.0 (0, 11, 4, 5) 0.1 ''' if weightAlgorithm is None: weightAlgorithm = self.quarterLengthOnly presentPCs = {} self.positionInMeasure = 0 self.numberOfElementsInMeasure = len(measureObject) for i, c in enumerate(measureObject): self.positionInMeasure = i p = tuple({x.pitchClass for x in c.pitches}) if p not in presentPCs: presentPCs[p] = 0.0 presentPCs[p] += weightAlgorithm(c) self.positionInMeasure = 0 self.numberOfElementsInMeasure = 0 return presentPCs
[docs] def fillBassGaps(self, scoreTree, partwiseTrees): def procedure(timespan): verticality = scoreTree.getVerticalityAt(timespan.offset) return verticality.bassTimespan for unused_part, subtree in partwiseTrees.items(): timespanList = list(subtree) for bassTimespan, group_generator in itertools.groupby(timespanList, procedure): group = list(group_generator) if bassTimespan is None: continue if bassTimespan.offset < group[0].offset: beatStrength = bassTimespan.element.beatStrength offset = bassTimespan.offset previousTimespan = scoreTree.findPreviousPitchedTimespanInSameStreamByClass( group[0] ) if previousTimespan is not None: if previousTimespan.endTime > group[0].offset: msg = ( 'Timespan offset errors: previousTimespan.endTime, ' + str(previousTimespan.endTime) + ' should be before ' + str(group[0].offset) + ' previousTimespan: ' + repr(previousTimespan) + ' groups: ' + repr(group) + ' group[0]: ' + repr(group[0]) ) print(msg) # raise ChordReducerException(msg) offset = max(offset, previousTimespan.endTime) scoreTree.removeTimespan(group[0]) subtree.removeTimespan(group[0]) newTimespan = group[0].new(offset=offset) newTimespan.beatStrength = beatStrength scoreTree.insert(newTimespan) subtree.insert(newTimespan) group[0] = newTimespan if group[-1].endTime < bassTimespan.endTime: endTime = bassTimespan.endTime scoreTree.removeTimespan(group[-1]) subtree.removeTimespan(group[-1]) newTimespan = group[-1].new( endTime=endTime, ) scoreTree.insert(newTimespan) subtree.insert(newTimespan) group[-1] = newTimespan for i in range(len(group) - 1): timespanOne, timespanTwo = group[i], group[i + 1] if (timespanOne.pitches == timespanTwo.pitches or timespanOne.endTime != timespanTwo.offset): newTimespan = timespanOne.new(endTime=timespanTwo.endTime) group[i] = newTimespan group[i + 1] = newTimespan scoreTree.removeTimespanList((timespanOne, timespanTwo)) subtree.removeTimespanList((timespanOne, timespanTwo)) scoreTree.insert(newTimespan) subtree.insert(newTimespan)
[docs] def fillMeasureGaps(self, scoreTree, partwiseTrees): r''' Fills measure gaps in `tree`. ''' for unused_part, subtree in partwiseTrees.items(): toRemove = set() toInsert = set() for unused_measureNumber, group_generator in itertools.groupby( subtree, lambda x: x.measureNumber ): group = list(group_generator) for i in range(len(group) - 1): timespanOne, timespanTwo = group[i], group[i + 1] if (timespanOne.pitches == timespanTwo.pitches or timespanOne.endTime != timespanTwo.offset): newTimespan = timespanOne.new(endTime=timespanTwo.endTime) group[i] = newTimespan group[i + 1] = newTimespan toInsert.add(newTimespan) toRemove.add(timespanOne) toRemove.add(timespanTwo) if group[0].offset != group[0].parentOffset: newTimespan = group[0].new(offset=group[0].parentOffset) newTimespan.beatStrength = 1.0 toRemove.add(group[0]) toInsert.add(newTimespan) group[0] = newTimespan if group[-1].endTime != group[-1].parentEndTime: newTimespan = group[-1].new( endTime=group[-1].parentEndTime, ) toRemove.add(group[-1]) toInsert.add(newTimespan) group[-1] = newTimespan # The insertion list may contain timespans later marked for removal # Therefore insertion must occur before removals toInsert.difference_update(toRemove) scoreTree.insert(toInsert) scoreTree.removeTimespanList(toRemove) subtree.insert(toInsert) subtree.removeTimespanList(toRemove)
[docs] def fuseTimespansByPart(self, scoreTree, part): def procedure(timespan): measureNumber = timespan.measureNumber pitches = timespan.pitches return measureNumber, pitches mapping = scoreTree.toPartwiseTimespanTrees() subtree = mapping[part] timespanList = list(subtree) for unused_key, group in itertools.groupby(timespanList, procedure): # measureNumber, pitches = key group = list(group) if len(group) == 1: continue scoreTree.removeTimespanList(group) newTimespan = group[0].new( endTime=group[-1].endTime, ) scoreTree.insert(newTimespan)
[docs] def qlbsmpConsonance(self, chordObject): ''' Everything from before plus consonance ''' consonanceScore = 1.0 if chordObject.isConsonant() else 0.1 if self.positionInMeasure == self.numberOfElementsInMeasure - 1: # call beatStrength 1 weight = chordObject.quarterLength else: weight = self.quarterLengthBeatStrengthMeasurePosition(chordObject) weight *= consonanceScore return weight
[docs] def quarterLengthBeatStrength(self, chordObject): weight = chordObject.quarterLength * chordObject.beatStrength return weight
[docs] def quarterLengthBeatStrengthMeasurePosition(self, chordObject): if self.positionInMeasure == self.numberOfElementsInMeasure - 1: return chordObject.quarterLength # call beatStrength 1 else: return self.quarterLengthBeatStrength(chordObject)
[docs] def quarterLengthOnly(self, chordObject): return chordObject.quarterLength
[docs] def reduceMeasureToNChords( self, measureObject, maximumNumberOfChords=1, weightAlgorithm=None, trimBelow=0.25, ): ''' Reduces measure to `n` chords: >>> s = analysis.reduceChords.testMeasureStream1() >>> cr = analysis.reduceChords.ChordReducer() Reduce to a maximum of 3 chords; though here we will only get one because the other chord is below the trimBelow threshold. >>> newS = cr.reduceMeasureToNChords(s, 3, ... weightAlgorithm=cr.qlbsmpConsonance, ... trimBelow=0.3) >>> newS.show('text') {0.0} <music21.meter.TimeSignature 4/4> {0.0} <music21.chord.Chord C4 E4 G4 C5> >>> newS[-1].quarterLength 4.0 ''' # from music21 import note # if inputMeasure.isFlat is False: # measureObject = inputMeasure.flatten().notes # else: # measureObject = inputMeasure.notes chordWeights = self.computeMeasureChordWeights( measureObject.flatten().notes, weightAlgorithm, ) maximumNumberOfChords = min(maximumNumberOfChords, len(chordWeights)) sortedChordWeights = sorted( chordWeights, key=chordWeights.get, reverse=True, ) maxNChords = sortedChordWeights[:maximumNumberOfChords] if not maxNChords: r = note.Rest() r.quarterLength = measureObject.duration.quarterLength for c in measureObject: measureObject.remove(c) measureObject.insert(0, r) return measureObject maxChordWeight = chordWeights[maxNChords[0]] trimmedMaxChords = [] for pcTuples in maxNChords: if chordWeights[pcTuples] >= maxChordWeight * trimBelow: trimmedMaxChords.append(pcTuples) else: break currentGreedyChord = None currentGreedyChordPCs = None currentGreedyChordNewLength = 0.0 for c in measureObject.getElementsByClass(note.NotRest): p = tuple({x.pitchClass for x in c.pitches}) if p in trimmedMaxChords and p != currentGreedyChordPCs: # keep this chord if currentGreedyChord is None and c.offset != 0.0: currentGreedyChordNewLength = c.offset c.offset = 0.0 elif currentGreedyChord is not None: currentGreedyChord.quarterLength = currentGreedyChordNewLength currentGreedyChordNewLength = 0.0 currentGreedyChord = c for n in c: # n.tie = None if n.pitch.accidental is not None: n.pitch.accidental.displayStatus = None currentGreedyChordPCs = p currentGreedyChordNewLength += c.quarterLength else: currentGreedyChordNewLength += c.quarterLength measureObject.remove(c) if currentGreedyChord is not None: currentGreedyChord.quarterLength = currentGreedyChordNewLength # not read later - no need to clean up. # currentGreedyChordNewLength = 0.0 # even chord lengths for i in range(1, len(measureObject)): c = measureObject[i] cOffsetCurrent = c.offset cOffsetSyncop = cOffsetCurrent - int(cOffsetCurrent) if round(cOffsetSyncop, 3) in [0.250, 0.125, 0.333, 0.063, 0.062]: lastC = measureObject[i - 1] lastC.quarterLength -= cOffsetSyncop c.offset = int(cOffsetCurrent) c.quarterLength += cOffsetSyncop return measureObject
[docs] def removeNonChordTones(self, scoreTree): r''' Removes timespans containing passing and neighbor tones from `tree`. ''' for verticalities in scoreTree.iterateVerticalitiesNwise(n=3): horizontalities = scoreTree.unwrapVerticalities(verticalities) for unused_part, horizontality in horizontalities.items(): if (not horizontality.hasPassingTone and not horizontality.hasNeighborTone): continue elif horizontality[0].measureNumber != horizontality[1].measureNumber: continue merged = horizontality[0].new(endTime=horizontality[1].endTime) scoreTree.removeTimespanList((horizontality[0], horizontality[1])) scoreTree.insert(merged)
[docs] def removeShortTimespans(self, scoreTree, partwiseTrees, duration=0.5): r''' Removes timespans in `tree` shorter than `duration`. Special treatment is given to groups of short timespans if they take up an entire measure. In that case, the timespans with the most common sets of pitches are kept. ''' def procedure(timespan): measureNumber = timespan.measureNumber proc_isShort = timespan.quarterLength < duration verticality = scoreTree.getVerticalityAt(timespan.offset) proc_bassTimespan = verticality.bassTimespan if proc_bassTimespan is not None: if proc_bassTimespan.quarterLength < duration: proc_bassTimespan = None return measureNumber, proc_isShort, proc_bassTimespan for unused_part, subtree in partwiseTrees.items(): timespansToRemove = [] for key, group in itertools.groupby(subtree, procedure): unused_measureNumber, isShort, bassTimespan = key group = list(group) if not isShort: continue isEntireMeasure = False if group[0].offset == group[0].parentOffset: if group[-1].endTime == group[0].parentEndTime: isEntireMeasure = True if bassTimespan is not None: if group[0].offset == bassTimespan.offset: if group[-1].endTime == bassTimespan.endTime: isEntireMeasure = True if isEntireMeasure: counter = collections.Counter() for group_timespan in group: counter[group_timespan.pitches] += group_timespan.quarterLength bestPitches, unused_totalDuration = counter.most_common()[0] for group_timespan in group: if group_timespan.pitches != bestPitches: timespansToRemove.append(group_timespan) else: timespansToRemove.extend(group) scoreTree.removeTimespanList(timespansToRemove) subtree.removeTimespanList(timespansToRemove)
[docs] def removeVerticalDissonances( self, scoreTree=None, allowableChords=None, forbiddenChords=None, ): r''' Removes timespans in each dissonant verticality of `tree` whose pitches are above the lowest pitch in that verticality. ''' for verticality in scoreTree.iterateVerticalities(): isConsonant = False pitches = verticality.pitchSet intervalClassSet = self._getIntervalClassSet(pitches) # print(verticality, intervalClassSet, allowableChords, forbiddenChords) if allowableChords and intervalClassSet in allowableChords: isConsonant = True if verticality.toChord().isConsonant(): isConsonant = True if forbiddenChords and intervalClassSet in forbiddenChords: isConsonant = False if isConsonant: # print('\tCONSONANT') continue # print('\tNOT CONSONANT') pitchSet = verticality.pitchSet lowestPitch = min(pitchSet) for timespan in verticality.startTimespans: if min(timespan.pitches) != lowestPitch: scoreTree.removeTimespan(timespan)
[docs] def removeZeroDurationTimespans(self, scoreTree): zeroDurationTimespans = [x for x in scoreTree if x.quarterLength == 0] scoreTree.removeTimespanList(zeroDurationTimespans)
[docs] def splitByBass(self, scoreTree): parts = scoreTree.allParts() for part in parts: self.fuseTimespansByPart(scoreTree, part) mapping = scoreTree.toPartwiseTimespanTrees() bassPart = parts[-1] bassTree = mapping[bassPart] bassOffsets = bassTree.allOffsets() scoreTree.splitAt(bassOffsets)
# ----------------------------------------------------------------------------- class Test(unittest.TestCase): def testSimpleMeasure(self): # from music21 import chord s = stream.Measure() c1 = chord.Chord('C4 E4 G4 C5') c1.quarterLength = 2.0 c2 = chord.Chord('C4 E4 F4 B4') c3 = chord.Chord('C4 E4 G4 C5') for c in [c1, c2, c3]: s.append(c) class TestExternal(unittest.TestCase): show = True def testTrecentoMadrigal(self): from music21 import corpus score = corpus.parse('PMFC_06_Giovanni-05_Donna').measures(1, 10) # score = corpus.parse('bach/bwv846').measures(1, 19) # score = corpus.parse('bach/bwv66.6') # score = corpus.parse('beethoven/opus18no1', 2).measures(1, 30) # score = corpus.parse('beethoven/opus18no1', 2).measures(1, 8) # score = corpus.parse('PMFC_06_Giovanni-05_Donna').measures(90, 118) # score = corpus.parse('PMFC_06_Piero_1').measures(1, 10) # score = corpus.parse('PMFC_06-Jacopo').measures(1, 30) # score = corpus.parse('PMFC_12_13').measures(1, 40) # score = corpus.parse('monteverdi/madrigal.4.16.xml').measures(1, 8) chordReducer = ChordReducer() reduction = chordReducer.run( score, allowableChords=( chord.Chord('F#4 A4 C5'), ), closedPosition=True, forbiddenChords=None, maximumNumberOfChords=3, ) for part in reduction: score.insert(0, part) if self.show: score.show() # ----------------------------------------------------------------------------- # define presented order in documentation _DOC_ORDER: DocOrder = [] if __name__ == '__main__': # TestExternal().testTrecentoMadrigal() import music21 music21.mainTest(TestExternal)