diff --git a/tests/ui/timelines/harmony/test_harmony_ui.py b/tests/ui/timelines/harmony/test_harmony_ui.py index 5a2ddebf9..d9c3b4a30 100644 --- a/tests/ui/timelines/harmony/test_harmony_ui.py +++ b/tests/ui/timelines/harmony/test_harmony_ui.py @@ -23,6 +23,28 @@ def test_right_click(self, tlui): exec_mock.assert_called_once() +class TestInversionClamp: + def test_letter_symbol_does_not_crash_when_quality_loses_inversion(self, tlui): + harmony, _ = tlui.create_harmony(0, quality="dominant-seventh", inversion=3) + tlui.timeline.set_component_data(harmony.id, "quality", "major") + # Pre-fix: music21.chord.ChordException — no 3rd inversion on triad. + _ = tlui.get_element(harmony.id).letter_symbol + + def test_roman_numeral_label_does_not_crash_when_quality_loses_inversion( + self, tlui + ): + harmony, _ = tlui.create_harmony(0, quality="dominant-seventh", inversion=3) + tlui.timeline.set_component_data(harmony.id, "quality", "major") + _ = tlui.get_element(harmony.id).roman_numeral_label + + def test_letter_symbol_label_omits_dropped_inversion(self, tlui): + harmony, _ = tlui.create_harmony(0, quality="dominant-seventh", inversion=3) + tlui.timeline.set_component_data(harmony.id, "quality", "major") + # Major triad max inversion is 2, so the 3rd-inversion suffix + # (INVERSION_TO_INTERVAL[3] == 7) must not appear in the label. + assert "/&7" not in tlui.get_element(harmony.id).letter_symbol_label + + class TestCopyPaste: def test_paste_single_into_timeline(self, tlui, tilia_state): _, hui = tlui.create_harmony(0) diff --git a/tilia/ui/timelines/harmony/elements/harmony.py b/tilia/ui/timelines/harmony/elements/harmony.py index 6f7bfc7b9..9c8274735 100644 --- a/tilia/ui/timelines/harmony/elements/harmony.py +++ b/tilia/ui/timelines/harmony/elements/harmony.py @@ -7,6 +7,7 @@ from PySide6.QtWidgets import QGraphicsItem, QGraphicsTextItem from tilia.requests import Get, Post, get, post +from tilia.timelines.harmony.constants import get_inversion_amount from tilia.ui.coords import time_x_converter from tilia.ui.timelines.base.element import TimelineUIElement from tilia.ui.timelines.drag import DragManager @@ -69,6 +70,13 @@ def font_type(self): def key(self): return self.timeline_ui.get_key_by_time(self.get_data("time")) + @property + def clamped_inversion(self): + return min( + self.get_data("inversion"), + get_inversion_amount(self.get_data("quality")), + ) + @property def letter_symbol(self): symbol = music21.harmony.ChordSymbol( @@ -78,7 +86,7 @@ def letter_symbol(self): self.get_data("accidental"), ) + QUALITY_TO_ABBREVIATION[self.get_data("quality")], - inversion=self.get_data("inversion"), + inversion=self.clamped_inversion, ) applied_to = self.get_data("applied_to") if applied_to: @@ -119,7 +127,7 @@ def roman_numeral_label(self): self.get_data("quality"), self.key, self.get_data("applied_to"), - self.get_data("inversion"), + self.clamped_inversion, ) @property @@ -150,7 +158,7 @@ def letter_symbol_label(self): case 2: figure = figure.replace("##", "`#`#") - if inversion := self.get_data("inversion"): + if inversion := self.clamped_inversion: # bass_step = harmony.calculate.bass_step(self.get_data('step'), inversion) # figure += '/' + INT_TO_NOTE_NAME[bass_step] # TODO calculate bass note figure += "/&" + str(INVERSION_TO_INTERVAL[inversion])