Compare commits

...

2 Commits

Author SHA1 Message Date
kennethreitz 1ed90c72d6 Fix scale_diagram enharmonic matching — v0.43.1
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) <noreply@anthropic.com>
2026-05-29 11:44:46 -04:00
kennethreitz 41e8404624 docs: flip remaining guides to low-to-high fingerings (v0.43.0 follow-up)
The v0.43.0 commit updated fretboard.rst and chords.rst but missed the
fingering/tab examples in quickstart, cookbook, and nashville-blues-tabs.
Update those Fingering reprs and fb.tab()/scale_diagram blocks to the new
low-to-high default, and fix the "Reading Tab Notation" prose (chord tab
now lists strings low-to-high; high_to_low=True restores the old layout).

Part.to_tab() examples are unchanged (orientation-agnostic, high-on-top).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:40:46 -04:00
9 changed files with 87 additions and 47 deletions
+8
View File
@@ -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`
+6 -6
View File
@@ -111,19 +111,19 @@ Generate fingerings for guitar and ukulele with
>>> fb = Fretboard.guitar()
>>> fb.chord("C")
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
Fingering(E=x, A=3, D=2, G=0, B=1, e=0)
>>> fb.chord("G")
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
Fingering(E=3, A=2, D=0, G=0, B=0, e=3)
>>> fb.chord("Am")
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
Fingering(E=x, A=0, D=2, G=2, B=1, e=0)
>>> fb.chord("D")
Fingering(e=2, B=3, G=2, D=0, A=x, E=x)
Fingering(E=x, A=x, D=0, G=2, B=3, e=2)
>>> uke = Fretboard.ukulele()
>>> uke.chord("C")
Fingering(A=3, E=0, C=0, G=0)
Fingering(G=0, C=0, E=0, A=3)
>>> uke.chord("G")
Fingering(A=2, E=3, C=2, G=0)
Fingering(G=0, C=2, E=3, A=2)
Explore an Interval
-------------------
+33 -30
View File
@@ -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 |
B| - | - | - | D | Eb| E | - | - | - | - | A | - | - |
G| - | - | A | - | - | C | - | D | Eb| E | - | - | - |
D| - | - | - | - | A | - | - | C | - | D | Eb| E | - |
A| A | - | - | C | - | 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
@@ -243,30 +243,30 @@ Get the tab for any chord on any instrument:
>>> fb = Fretboard.guitar()
>>> print(fb.tab("C"))
C major
e|--0--
B|--1--
G|--0--
D|--2--
A|--3--
E|--x--
A|--3--
D|--2--
G|--0--
B|--1--
e|--0--
>>> print(fb.tab("Am"))
A minor
e|--0--
B|--1--
G|--2--
D|--2--
A|--0--
E|--x--
A|--0--
D|--2--
G|--2--
B|--1--
e|--0--
>>> print(fb.tab("E7"))
E dominant 7th
e|--0--
B|--0--
G|--1--
D|--0--
A|--2--
E|--0--
A|--2--
D|--0--
G|--1--
B|--0--
e|--0--
Works with any instrument:
@@ -275,24 +275,27 @@ Works with any instrument:
>>> uke = Fretboard.ukulele()
>>> print(uke.tab("C"))
C major
A|--3--
E|--0--
C|--0--
G|--0--
C|--0--
E|--0--
A|--3--
Reading Tab Notation
~~~~~~~~~~~~~~~~~~~~~
::
e|--0-- ← open string (don't fret, just pluck)
B|--1-- ← press fret 1
G|--0-- ← open string
D|--2-- ← press fret 2
A|--3-- ← press fret 3
E|--x-- ← muted (don't play this string)
A|--3-- ← press fret 3
D|--2-- ← press fret 2
G|--0-- ← open string
B|--1-- ← press fret 1
e|--0-- ← open string (don't fret, just pluck)
- Each line is a string (highest pitch at top, lowest at bottom)
- Each line is a string. Chord tab from ``fb.tab()`` lists strings
low-to-high (lowest pitch at top) by default since v0.43.0; pass
``high_to_low=True`` to the fretboard for the traditional
highest-pitch-on-top layout.
- Numbers are fret positions (0 = open, 1-24 = fretted)
- ``x`` means the string is muted / not played
- ``|`` marks measure boundaries in sequence tabs
+1 -1
View File
@@ -130,7 +130,7 @@ Guitar fingerings:
>>> fb = Fretboard.guitar()
>>> fb.chord("Am")
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
Fingering(E=x, A=0, D=2, G=2, B=1, e=0)
All of the above works without PortAudio, without sounddevice,
without any audio setup at all. It's pure Python music theory.
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+23 -7
View File
@@ -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)}|"
+13
View File
@@ -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():
Generated
+1 -1
View File
@@ -690,7 +690,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.43.0"
version = "0.43.1"
source = { editable = "." }
dependencies = [
{ name = "rich" },