Compare commits

...

2 Commits

Author SHA1 Message Date
kennethreitz 5aed586187 v0.8.2: Flat spellings in CHARTS acceptable_tone_names
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:52:50 -04:00
kennethreitz 09d90b3425 Use flat spellings in CHARTS acceptable_tone_names
NamedChord.acceptable_tones now uses prefer_flats based on circle-of-fifths
conventions. Cm7 shows (C, Eb, G, Bb) instead of (C, D#, G, A#).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:43:55 -04:00
5 changed files with 64 additions and 26 deletions
+1 -1
View File
@@ -139,7 +139,7 @@ Quality Intervals Example tones (from C)
('C', 'E', 'G')
>>> chart["Cm7"].acceptable_tone_names
('C', 'D#', 'G', 'A#')
('C', 'Eb', 'G', 'Bb')
Building Chords
---------------
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.8.1"
version = "0.8.2"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.8.1"
__version__ = "0.8.2"
from .tones import Tone, Interval
from .systems import System, SYSTEMS
+51 -13
View File
@@ -193,63 +193,101 @@ class NamedChord:
def __repr__(self):
return f"<NamedChord name={self.name!r}>"
@property
def _prefer_flats(self):
"""Determine whether this chord's tones should use flat spellings.
Uses the circle-of-fifths convention:
- Flat-root notes (Bb, Eb, Ab, Db, Gb) always prefer flats.
- Major-type qualities prefer flats for roots: F, Bb, Eb, Ab, Db, Gb.
- Minor-type qualities prefer flats for roots: D, G, C, F, Bb, Eb, Ab.
"""
# Root is itself a flat note — always prefer flats
if "b" in self.tone_name and self.tone_name != "B":
return True
_FLAT_MAJOR_ROOTS = {"F", "Bb", "Eb", "Ab", "Db", "Gb"}
_FLAT_MINOR_ROOTS = {"D", "G", "C", "F", "Bb", "Eb", "Ab"}
# Dominant 7th/9th chords contain a minor 7th (b7), so they
# follow the same flat-preference roots as minor chords.
_FLAT_DOMINANT_ROOTS = {"C", "F", "G", "Bb", "Eb", "Ab", "Db", "Gb"}
minor_qualities = {"m", "m6", "m7", "m9", "dim"}
dominant_qualities = {"7", "9"}
major_qualities = {"", "maj", "5", "maj7", "maj9"}
if self.quality in minor_qualities and self.tone_name in _FLAT_MINOR_ROOTS:
return True
if self.quality in dominant_qualities and self.tone_name in _FLAT_DOMINANT_ROOTS:
return True
if self.quality in major_qualities and self.tone_name in _FLAT_MAJOR_ROOTS:
return True
return False
@property
def acceptable_tones(self):
acceptable = [self.tone]
flats = self._prefer_flats
if self.quality == "maj":
# Major triad: root, major 3rd, perfect 5th
acceptable += [self.tone.add(4), self.tone.add(7)]
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats)]
elif self.quality == "m":
# Minor triad: root, minor 3rd, perfect 5th
acceptable += [self.tone.add(3), self.tone.add(7)]
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats)]
elif self.quality == "5":
# Power chord: root, perfect 5th
acceptable += [self.tone.add(7)]
acceptable += [self.tone.add(7, prefer_flats=flats)]
elif self.quality == "7":
# Dominant 7th: root, major 3rd, perfect 5th, minor 7th
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(10)]
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats)]
elif self.quality == "9":
# Dominant 9th: root, major 3rd, perfect 5th, minor 7th, major 9th
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(10), self.tone.add(2)]
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats), self.tone.add(2, prefer_flats=flats)]
elif self.quality == "dim":
# Diminished: root, minor 3rd, diminished 5th
acceptable += [self.tone.add(3), self.tone.add(6)]
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(6, prefer_flats=flats)]
elif self.quality == "m6":
# Minor 6th: root, minor 3rd, perfect 5th, major 6th
acceptable += [self.tone.add(3), self.tone.add(7), self.tone.add(9)]
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(9, prefer_flats=flats)]
elif self.quality == "m7":
# Minor 7th: root, minor 3rd, perfect 5th, minor 7th
acceptable += [self.tone.add(3), self.tone.add(7), self.tone.add(10)]
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats)]
elif self.quality == "m9":
# Minor 9th: root, minor 3rd, perfect 5th, minor 7th, major 9th
acceptable += [self.tone.add(3), self.tone.add(7), self.tone.add(10), self.tone.add(2)]
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats), self.tone.add(2, prefer_flats=flats)]
elif self.quality == "maj7":
# Major 7th: root, major 3rd, perfect 5th, major 7th
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(11)]
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(11, prefer_flats=flats)]
elif self.quality == "maj9":
# Major 9th: root, major 3rd, perfect 5th, major 7th, major 9th
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(11), self.tone.add(2)]
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(11, prefer_flats=flats), self.tone.add(2, prefer_flats=flats)]
else:
# Default (no quality): major triad
acceptable += [self.tone.add(4), self.tone.add(7)]
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats)]
return tuple(acceptable)
@property
def acceptable_tone_names(self):
return tuple([tone.name for tone in self.acceptable_tones])
names = [tone.name for tone in self.acceptable_tones]
# The root tone is stored internally with sharp spelling (e.g. A#
# for Bb) via flat_to_sharp mapping; restore the original flat name.
if names and names[0] != self.tone_name:
names[0] = self.tone_name
return tuple(names)
def _possible_fingerings(self, *, fretboard):
# Check the _possible_cache first
+10 -10
View File
@@ -417,7 +417,7 @@ def test_named_chord_c_minor_tones():
cm = NamedChord(tone_name="C", quality="m")
names = cm.acceptable_tone_names
assert "C" in names
assert "D#" in names # Eb enharmonic
assert "Eb" in names # minor 3rd
assert "G" in names
@@ -435,24 +435,24 @@ def test_named_chord_dominant_7th():
assert "C" in names
assert "E" in names # major 3rd
assert "G" in names # perfect 5th
assert "A#" in names # minor 7th (Bb)
assert "Bb" in names # minor 7th
def test_named_chord_diminished():
cdim = NamedChord(tone_name="C", quality="dim")
names = cdim.acceptable_tone_names
assert "C" in names
assert "D#" in names # minor 3rd (Eb)
assert "F#" in names # diminished 5th (Gb)
assert "Eb" in names # minor 3rd
assert "Gb" in names # diminished 5th
def test_named_chord_minor_7th():
cm7 = NamedChord(tone_name="C", quality="m7")
names = cm7.acceptable_tone_names
assert "C" in names
assert "D#" in names # minor 3rd
assert "Eb" in names # minor 3rd
assert "G" in names # perfect 5th
assert "A#" in names # minor 7th
assert "Bb" in names # minor 7th
def test_named_chord_major_7th():
@@ -1258,7 +1258,7 @@ def test_named_chord_m6_tones():
cm6 = NamedChord(tone_name="C", quality="m6")
names = cm6.acceptable_tone_names
assert "C" in names
assert "D#" in names # minor 3rd
assert "Eb" in names # minor 3rd
assert "G" in names # perfect 5th
assert "A" in names # major 6th
assert len(names) == 4
@@ -1268,9 +1268,9 @@ def test_named_chord_m9_tones():
cm9 = NamedChord(tone_name="C", quality="m9")
names = cm9.acceptable_tone_names
assert "C" in names
assert "D#" in names # minor 3rd
assert "Eb" in names # minor 3rd
assert "G" in names # perfect 5th
assert "A#" in names # minor 7th
assert "Bb" in names # minor 7th
assert "D" in names # major 9th
assert len(names) == 5
@@ -1292,7 +1292,7 @@ def test_named_chord_9_tones():
assert "C" in names
assert "E" in names # major 3rd
assert "G" in names # perfect 5th
assert "A#" in names # minor 7th
assert "Bb" in names # minor 7th
assert "D" in names # major 9th
assert len(names) == 5