From 1ed90c72d686b4190cec4e5dde98c8e67c36b972 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Fri, 29 May 2026 11:44:46 -0400 Subject: [PATCH] =?UTF-8?q?Fix=20scale=5Fdiagram=20enharmonic=20matching?= =?UTF-8?q?=20=E2=80=94=20v0.43.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scale notes spelled with flats (e.g. the Eb blue note in the blues scale) were silently dropped from scale_diagram() because the fretboard spells that pitch as D#, so the string comparison never matched. Match notes enharmonically via the system's canonical name and display them using the scale's own spelling. Pre-existing bug (also in 0.42.x), surfaced while flipping the docs for the low-to-high release. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 8 ++++++++ docs/guide/nashville-blues-tabs.rst | 12 ++++++------ pyproject.toml | 2 +- pytheory/__init__.py | 2 +- pytheory/chords.py | 30 ++++++++++++++++++++++------- test_pytheory.py | 13 +++++++++++++ uv.lock | 2 +- 7 files changed, 53 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b059c..fb5f493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to PyTheory are documented here. +## 0.43.1 + +- **Fix `Fretboard.scale_diagram()` enharmonic matching.** Scale notes + spelled with flats (e.g. the `Eb` blue note in the blues scale) were + silently omitted from the diagram, because the fretboard spells that + pitch as `D#`. Notes are now matched enharmonically (by pitch) and + displayed using the scale's own spelling. + ## 0.43.0 - **BREAKING — fingerings now read low-to-high by default.** `Fretboard` diff --git a/docs/guide/nashville-blues-tabs.rst b/docs/guide/nashville-blues-tabs.rst index e6245ce..4952c0f 100644 --- a/docs/guide/nashville-blues-tabs.rst +++ b/docs/guide/nashville-blues-tabs.rst @@ -208,12 +208,12 @@ Visualize the blues scale on guitar to see the patterns: >>> blues = TonedScale(tonic="A4", system="blues")["blues"] >>> print(fb.scale_diagram(blues, frets=12)) 0 1 2 3 4 5 6 7 8 9 10 11 12 - E| - | - | - | - | - | A | - | - | C | - | D | Eb| E | - A| A | - | - | C | - | D | Eb| E | - | - | - | - | A | - D| - | - | - | - | A | - | - | C | - | D | Eb| E | - | - G| - | - | A | - | - | C | - | D | Eb| E | - | - | - | - B| - | - | - | D | Eb| E | - | - | - | - | A | - | - | - E| - | - | - | - | - | A | - | - | C | - | D | Eb| E | + E| E | - | - | G | - | A | - | - | C | - | D | Eb| E | + A| A | - | - | C | - | D | Eb| E | - | - | G | - | A | + D| D | Eb| E | - | - | G | - | A | - | - | C | - | D | + G| G | - | A | - | - | C | - | D | Eb| E | - | - | G | + B| - | C | - | D | Eb| E | - | - | G | - | A | - | - | + E| E | - | - | G | - | A | - | - | C | - | D | Eb| E | The minor pentatonic (same scale without the Eb) is the most-played scale in rock guitar. Add the blue note and you have the full blues diff --git a/pyproject.toml b/pyproject.toml index 20c4884..e22e5f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytheory" -version = "0.43.0" +version = "0.43.1" description = "Music Theory for Humans" readme = "README.md" license = "MIT" diff --git a/pytheory/__init__.py b/pytheory/__init__.py index 12fbb46..9c4335a 100644 --- a/pytheory/__init__.py +++ b/pytheory/__init__.py @@ -1,6 +1,6 @@ """PyTheory: Music Theory for Humans.""" -__version__ = "0.43.0" +__version__ = "0.43.1" from .tones import Tone, Interval from .systems import System, SYSTEMS, TET diff --git a/pytheory/chords.py b/pytheory/chords.py index 7c80ac8..2debf17 100644 --- a/pytheory/chords.py +++ b/pytheory/chords.py @@ -1867,10 +1867,24 @@ class Fretboard: >>> am = Chord.from_symbol("Am") >>> print(fb.scale_diagram(pentatonic, frets=5, chord=am)) """ - scale_notes = set(scale.note_names) + # Match notes enharmonically: the fretboard spells tones with + # sharps (e.g. D#), but a scale may use flats (e.g. Eb). Compare + # via the system's canonical name so Eb and D# count as the same + # pitch — and display using the scale's own spelling. + _system = self._tones[0].system + def _resolve(name): + resolved = _system.resolve_name(name) + return resolved if resolved is not None else name + + # Map canonical pitch -> the scale's preferred spelling for display. + scale_display = {} + for n in scale.note_names: + scale_display.setdefault(_resolve(n), n) + scale_notes = set(scale_display) + chord_notes = set() if chord is not None: - chord_notes = {t.name for t in chord.tones} + chord_notes = {_resolve(t.name) for t in chord.tones} max_name = max(len(t.name) for t in self.tones) lines = [] @@ -1885,13 +1899,15 @@ class Fretboard: fret_marks = [] for f in range(frets + 1): note = tone.add(f) - if note.name in scale_notes: - if chord_notes and note.name in chord_notes: - fret_marks.append(f" {note.name.upper():<2s}") + key = _resolve(note.name) + if key in scale_notes: + label = scale_display[key] + if chord_notes and key in chord_notes: + fret_marks.append(f" {label.upper():<2s}") elif chord_notes: - fret_marks.append(f" {note.name.lower():<2s}") + fret_marks.append(f" {label.lower():<2s}") else: - fret_marks.append(f" {note.name:<2s}") + fret_marks.append(f" {label:<2s}") else: fret_marks.append(" - ") line = f"{tone.name:>{max_name}}|{'|'.join(fret_marks)}|" diff --git a/test_pytheory.py b/test_pytheory.py index 1a43e41..57a37dd 100644 --- a/test_pytheory.py +++ b/test_pytheory.py @@ -3522,6 +3522,19 @@ def test_scale_diagram(): assert len(lines) == 7 +def test_scale_diagram_enharmonic_flat_note(): + """A flat-spelled scale note (e.g. the blues Eb) must render even + though the fretboard spells that pitch as D#.""" + fb = Fretboard.guitar() + blues = TonedScale(tonic="A4", system="blues")["blues"] + assert "Eb" in blues.note_names + diagram = fb.scale_diagram(blues, frets=12) + # The blue note shows up using the scale's own (flat) spelling, + # never the fretboard's sharp spelling. + assert "Eb" in diagram + assert "D#" not in diagram + + # ── Coverage gap tests ───────────────────────────────────────────────────── def test_tone_init_octave_parsed_from_name(): diff --git a/uv.lock b/uv.lock index 13998bf..b284c1d 100644 --- a/uv.lock +++ b/uv.lock @@ -690,7 +690,7 @@ wheels = [ [[package]] name = "pytheory" -version = "0.43.0" +version = "0.43.1" source = { editable = "." } dependencies = [ { name = "rich" },