Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions music21/chord/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1668,7 +1668,7 @@ def _findRoot(self) -> pitch.Pitch:
Generally use root() instead, since if a chord doesn't know its root,
root() will run ._findRoot() automatically.
'''
def rootnessFunction(rootThirdList):
def rootnessFunction(rootThirdList, is_sus4_chord=False):
'''
Returns a value for how likely this pitch is to be a root given the
number of thirds and fifths above it.
Expand All @@ -1687,6 +1687,14 @@ def rootnessFunction(rootThirdList):
for root_index, val in enumerate(rootThirdList):
if val is True:
score += 1 / (root_index + 6)
# sus4 bonus: for sus4 chords (no note has both a 3rd and 5th above it),
# reward a note that has a 4th (index 4) and 5th (index 1) above it.
# orderedChordSteps = (3, 5, 7, 2, 4, 6) so index 0=3rd, 1=5th, 4=4th
if is_sus4_chord:
has_fifth = rootThirdList[1] if len(rootThirdList) > 1 else False
has_fourth = rootThirdList[4] if len(rootThirdList) > 4 else False
if has_fourth and has_fifth:
score += 1 / 6 # same weight as having a third
return score

# FIND ROOT FAST -- for cases where one note has perfectly stacked
Expand Down Expand Up @@ -1735,6 +1743,12 @@ def rootnessFunction(rootThirdList):
rootnessFunctionScores = []
orderedChordSteps = (3, 5, 7, 2, 4, 6)

# Detect sus4 chords: no note has both a 3rd (step+2) and 5th (step+4) above it
is_sus4_chord = not any(
(sn + 2) % 7 in stepNumsToPitches and (sn + 4) % 7 in stepNumsToPitches
for sn in stepNums
)

for p in nonDuplicatingPitches:
currentListOfThirds = []
this_step_num = pitch.STEP_TO_DNN_OFFSET[p.step]
Expand All @@ -1744,7 +1758,7 @@ def rootnessFunction(rootThirdList):
else:
currentListOfThirds.append(False)

rootnessScore = rootnessFunction(currentListOfThirds)
rootnessScore = rootnessFunction(currentListOfThirds, is_sus4_chord=is_sus4_chord)
rootnessFunctionScores.append(rootnessScore)

mostRootyIndex = rootnessFunctionScores.index(max(rootnessFunctionScores))
Expand Down
25 changes: 25 additions & 0 deletions music21/test/test_chord.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,31 @@ def testConstruction(self):
self.assertEqual(unscrambledChord3.pitches[3].name, 'F')
self.assertEqual(unscrambledChord3.pitches[4].name, 'A-')

def testSus4RootFinding(self):
# Regression test for https://github.com/cuthbertLab/music21/issues/1650
# sus4 chords should correctly identify their root

# Basic sus4 seventh chord from the issue report
sus4_seventh = chord.Chord('F B- C E-')
self.assertEqual(sus4_seventh.root().name, 'F')

# Simple sus4 triad
sus4_triad = chord.Chord('C F G')
self.assertEqual(sus4_triad.root().name, 'C')

# sus4 in different inversion (F in bass)
sus4_inv = chord.Chord('C F G B-')
self.assertEqual(sus4_inv.root().name, 'C')

# Regular chords should still work correctly
self.assertEqual(chord.Chord('C E G').root().name, 'C')
self.assertEqual(chord.Chord('C E- G').root().name, 'C')
self.assertEqual(chord.Chord('C E G B-').root().name, 'C')

# C11 chord (has both 3rd and 11th) should still return C, not F
c11 = chord.Chord('C E G B- D F')
self.assertEqual(c11.root().name, 'C')

def testEnharmonicSimplification(self):
eFlat = note.Note(63)
self.assertEqual(eFlat.pitch.name, 'E-')
Expand Down
Loading