mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 06:46:14 +00:00
Low-to-high fingerings by default (high_to_low opt-out) — v0.43.0
Fretboard string lists and Fingering positions/string-names now read low-to-high (lowest-pitched string first), matching how chord diagrams and tablature are conventionally written. Pass high_to_low=True to any fretboard constructor to restore the pre-0.43 high-to-low behavior. Design: each board keeps a private canonical (high-to-low) tone store so the fingering scorer and GUITAR_OVERRIDES table stay untouched; a single _orient() helper re-orients at the user-facing boundary (and, being self-inverse, also canonicalizes custom tuning/position input). Fingering carries its own orientation flag and presents oriented positions/names via properties. The fingering cache key now includes orientation so the two orderings don't collide. to_tab() and Part.strum() now sort by pitch internally, so their output is identical regardless of board orientation. - All 25 instrument presets gain a high_to_low param, routed through a canonical build path. - Tests updated for the new default; added orientation-specific tests. - Docs/examples flipped to low-to-high; chord_charts.py example now uses the built-in Fingering.tab() instead of a hand-rolled renderer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,29 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.43.0
|
||||
|
||||
- **BREAKING — fingerings now read low-to-high by default.** `Fretboard`
|
||||
string lists and `Fingering` positions/string-names now run from the
|
||||
**lowest-pitched string first** (e.g. standard guitar reads `E A D G B E`),
|
||||
matching how chord diagrams and tablature are conventionally written.
|
||||
Previously they ran high-to-low (`E B G D A E`). This affects
|
||||
`Fretboard.tones`, iteration over a fretboard, `repr`, `chord()`, `tab()`,
|
||||
`chart()`, and `fingering()` output.
|
||||
|
||||
To restore the pre-0.43 high-to-low behavior, pass **`high_to_low=True`**
|
||||
to any fretboard constructor — `Fretboard.guitar(high_to_low=True)`,
|
||||
`Fretboard(tones=..., high_to_low=True)`, and likewise on every instrument
|
||||
preset (`bass`, `ukulele`, `mandolin`, … `keyboard`).
|
||||
|
||||
The flip also applies to **input**: a custom tuning tuple passed to
|
||||
`Fretboard.guitar(...)` and manual fret positions passed to
|
||||
`fingering(*positions)` are now read in the board's orientation
|
||||
(low-to-high by default).
|
||||
|
||||
`to_tab()` and `Part.strum()` are unaffected — they sort by pitch
|
||||
internally and produce identical output regardless of orientation.
|
||||
|
||||
## 0.42.1
|
||||
|
||||
- **Fretboard tuning support** — `to_tab()` now accepts `Fretboard` objects as
|
||||
|
||||
@@ -94,11 +94,11 @@ PyTheory includes 144 pre-built chords (12 roots x 12 qualities):
|
||||
|
||||
>>> 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("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("G7")
|
||||
Fingering(e=1, B=0, G=0, D=0, A=2, E=3)
|
||||
Fingering(E=3, A=2, D=0, G=0, B=0, e=1)
|
||||
|
||||
You can also build chords directly with ``Chord.from_name()``:
|
||||
|
||||
|
||||
+40
-31
@@ -18,19 +18,28 @@ positions are just semitone steps along the fingerboard.
|
||||
Guitars
|
||||
-------
|
||||
|
||||
`Standard guitar tuning <https://en.wikipedia.org/wiki/Guitar_tunings>`_
|
||||
(high to low)::
|
||||
`Standard guitar tuning <https://en.wikipedia.org/wiki/Guitar_tunings>`_::
|
||||
|
||||
String 1: E4 (highest)
|
||||
String 2: B3
|
||||
String 3: G3
|
||||
String 4: D3
|
||||
String 5: A2
|
||||
String 6: E2 (lowest)
|
||||
String 5: A2
|
||||
String 4: D3
|
||||
String 3: G3
|
||||
String 2: B3
|
||||
String 1: E4 (highest)
|
||||
|
||||
This tuning uses intervals of a perfect 4th (5 semitones) between most
|
||||
strings, except between G and B which is a major 3rd (4 semitones).
|
||||
|
||||
.. note::
|
||||
|
||||
Since **v0.43.0**, fingerings and string lists read **low to high**
|
||||
(lowest-pitched string first) by default — matching how chord
|
||||
diagrams and tab are conventionally written. To get the pre-0.43
|
||||
high-to-low order, pass ``high_to_low=True`` to any fretboard
|
||||
constructor, e.g. ``Fretboard.guitar(high_to_low=True)``. A custom
|
||||
tuning tuple and manual ``fingering()`` positions are likewise read
|
||||
in the board's orientation.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard
|
||||
@@ -192,12 +201,12 @@ on any instrument. It scores each possibility by:
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> f = fb.chord("C")
|
||||
>>> f
|
||||
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)
|
||||
|
||||
>>> f['A']
|
||||
3
|
||||
>>> f[1]
|
||||
1
|
||||
3
|
||||
|
||||
>>> f.identify()
|
||||
'C major'
|
||||
@@ -210,11 +219,11 @@ You can also go from fret positions to chord identification:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # "What chord am I playing?"
|
||||
>>> # "What chord am I playing?" (positions read low to high)
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> f = fb.fingering(0, 0, 0, 2, 2, 0)
|
||||
>>> f = fb.fingering(0, 2, 2, 0, 0, 0)
|
||||
>>> f
|
||||
Fingering(e=0, B=0, G=0, D=2, A=2, E=0)
|
||||
Fingering(E=0, A=2, D=2, G=0, B=0, e=0)
|
||||
>>> f.identify()
|
||||
'E minor'
|
||||
|
||||
@@ -223,14 +232,14 @@ Reading Fingerings
|
||||
|
||||
Each position is labeled with its string name. Duplicate string names
|
||||
are disambiguated — on a standard guitar, high E appears as ``e`` and
|
||||
low E as ``E``::
|
||||
low E as ``E``. Strings read low to high (lowest first)::
|
||||
|
||||
e|--0-- (open — E)
|
||||
B|--1-- (fret 1 — C)
|
||||
G|--0-- (open — G)
|
||||
D|--2-- (fret 2 — E)
|
||||
E|--x-- (muted — low E)
|
||||
A|--3-- (fret 3 — C)
|
||||
E|--x-- (muted)
|
||||
D|--2-- (fret 2 — E)
|
||||
G|--0-- (open — G)
|
||||
B|--1-- (fret 1 — C)
|
||||
e|--0-- (open — high E)
|
||||
|
||||
A value of ``x`` (``None``) means the string is muted (not played).
|
||||
|
||||
@@ -243,12 +252,12 @@ For a more visual representation, use ``tab()``:
|
||||
|
||||
>>> 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--
|
||||
|
||||
Generating Full Charts
|
||||
----------------------
|
||||
@@ -261,7 +270,7 @@ Generate fingerings for every chord at once:
|
||||
>>> chart = fb.chart()
|
||||
|
||||
>>> chart["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)
|
||||
|
||||
>>> # Works with any instrument
|
||||
>>> uke_chart = Fretboard.ukulele().chart()
|
||||
@@ -303,19 +312,19 @@ Any instrument can be modeled with custom string tunings:
|
||||
|
||||
>>> from pytheory import Tone, Fretboard
|
||||
|
||||
>>> # Baritone ukulele (DGBE — top 4 guitar strings)
|
||||
>>> # Baritone ukulele (DGBE — top 4 guitar strings, low to high)
|
||||
>>> bari_uke = Fretboard(tones=[
|
||||
... Tone.from_string("E4"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("D3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("E4"),
|
||||
... ])
|
||||
|
||||
>>> # Tres cubano (Cuban guitar, 3 doubled courses)
|
||||
>>> # Tres cubano (Cuban guitar, 3 doubled courses, low to high)
|
||||
>>> tres = Fretboard(tones=[
|
||||
... Tone.from_string("E4"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("E4"),
|
||||
... ])
|
||||
|
||||
If it has strings, you can model it. Define the tuning, and PyTheory handles the rest -- fingerings, charts, scale diagrams, all of it. Got a weird instrument or a custom tuning? That's what the ``Fretboard`` constructor is for.
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
from pytheory import Tone, Fretboard, CHARTS
|
||||
from pytheory import Fretboard, CHARTS
|
||||
|
||||
# Create standard tuning (from high E to low E)
|
||||
standard_tuning = [
|
||||
Tone.from_string("E4"), # High E
|
||||
Tone.from_string("B3"), # B
|
||||
Tone.from_string("G3"), # G
|
||||
Tone.from_string("D3"), # D
|
||||
Tone.from_string("A2"), # A
|
||||
Tone.from_string("E2"), # Low E
|
||||
]
|
||||
|
||||
# Create fretboard with standard tuning
|
||||
fretboard = Fretboard(tones=standard_tuning)
|
||||
# Standard guitar fretboard. Since v0.43.0 fingerings read low to high
|
||||
# (low E first) by default — exactly how tab is conventionally written.
|
||||
fretboard = Fretboard.guitar()
|
||||
|
||||
# Define flat to sharp note mappings (updated to include all possible flats)
|
||||
flat_to_sharp = {"Ab": "G#", "Bb": "A#", "Db": "C#", "Eb": "D#", "Gb": "F#"}
|
||||
@@ -26,34 +17,6 @@ print("Standard Guitar Chord Charts:")
|
||||
print("-" * 30)
|
||||
|
||||
|
||||
def fingering_to_tab(fingering):
|
||||
if not fingering:
|
||||
return ""
|
||||
|
||||
# Create 6 strings of dashes, representing the guitar strings
|
||||
strings = ["-" * 15 for _ in range(6)]
|
||||
|
||||
# For each string (starting from high E)
|
||||
for string_num, fret in enumerate(fingering):
|
||||
if fret is not None:
|
||||
# Place the fret number at the correct position
|
||||
if fret == 0:
|
||||
strings[string_num] = "0" + strings[string_num][1:]
|
||||
else:
|
||||
strings[string_num] = (
|
||||
"-" * (fret - 1) + str(fret) + strings[string_num][fret:]
|
||||
)
|
||||
|
||||
# Combine strings with newlines, and add string names
|
||||
tab = "e|" + strings[0] + "\n"
|
||||
tab += "B|" + strings[1] + "\n"
|
||||
tab += "G|" + strings[2] + "\n"
|
||||
tab += "D|" + strings[3] + "\n"
|
||||
tab += "A|" + strings[4] + "\n"
|
||||
tab += "E|" + strings[5] + "\n"
|
||||
return tab
|
||||
|
||||
|
||||
for chord_name in all_chords:
|
||||
# Store original chord name for lookup
|
||||
lookup_name = chord_name
|
||||
@@ -74,7 +37,7 @@ for chord_name in all_chords:
|
||||
try:
|
||||
fingering = chord.fingering(fretboard=fretboard)
|
||||
print(f"\n{display_name}:")
|
||||
print(fingering_to_tab(fingering))
|
||||
print(fingering.tab())
|
||||
except Exception as e:
|
||||
print(f"{display_name}: Unable to calculate fingering - {str(e)}")
|
||||
# Add more detailed debug information
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.42.1"
|
||||
version = "0.43.0"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.42.1"
|
||||
__version__ = "0.43.0"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+77
-42
@@ -13,7 +13,8 @@ STANDARD_GUITAR_TUNING = ("E4", "B3", "G3", "D3", "A2", "E2")
|
||||
|
||||
# Curated override fingerings for common guitar chords in standard tuning.
|
||||
# Key: chord name, Value: tuple of fret positions (-1 = muted string).
|
||||
# Order is high-to-low (matching Fretboard.guitar() string order).
|
||||
# Order is canonical high-to-low (high-E first); Fingering re-orients these
|
||||
# to the board's display orientation (low-to-high by default since v0.43.0).
|
||||
GUITAR_OVERRIDES = {
|
||||
"C": (0, 1, 0, 2, 3, -1),
|
||||
"D": (2, 3, 2, 0, -1, -1),
|
||||
@@ -60,11 +61,16 @@ class Fingering:
|
||||
3
|
||||
"""
|
||||
|
||||
def __init__(self, positions: tuple, string_names: tuple[str, ...], *, fretboard=None) -> None:
|
||||
self.positions = tuple(positions)
|
||||
def __init__(self, positions: tuple, string_names: tuple[str, ...], *,
|
||||
fretboard=None, high_to_low: bool = True) -> None:
|
||||
# `positions` / `string_names` arrive in canonical (high-to-low)
|
||||
# order; `high_to_low` controls only how they're presented. The
|
||||
# default (True) keeps standalone construction high-to-low.
|
||||
self.high_to_low = high_to_low
|
||||
self._positions = tuple(positions)
|
||||
self._fretboard = fretboard
|
||||
# Disambiguate duplicate names: for standard guitar tuning
|
||||
# (high-to-low), the first occurrence of a duplicate becomes
|
||||
# Disambiguate duplicate names in canonical (high-to-low) order:
|
||||
# the first (higher-pitched) occurrence of a duplicate becomes
|
||||
# lowercase (e.g. high E → 'e') while the last keeps uppercase.
|
||||
from collections import Counter
|
||||
name_counts = Counter(string_names)
|
||||
@@ -77,8 +83,22 @@ class Fingering:
|
||||
else:
|
||||
unique_names.append(name)
|
||||
|
||||
self.string_names = tuple(unique_names)
|
||||
self._map = dict(zip(self.string_names, self.positions))
|
||||
self._string_names = tuple(unique_names)
|
||||
self._map = dict(zip(self._string_names, self._positions))
|
||||
|
||||
def _orient(self, seq):
|
||||
"""Re-orient a canonical (high-to-low) sequence for display."""
|
||||
return tuple(seq) if self.high_to_low else tuple(reversed(seq))
|
||||
|
||||
@property
|
||||
def positions(self) -> tuple:
|
||||
"""Fret positions in this fingering's orientation (low-to-high by default)."""
|
||||
return self._orient(self._positions)
|
||||
|
||||
@property
|
||||
def string_names(self) -> tuple:
|
||||
"""String names in this fingering's orientation (low-to-high by default)."""
|
||||
return self._orient(self._string_names)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
pairs = ", ".join(
|
||||
@@ -114,11 +134,12 @@ class Fingering:
|
||||
"""
|
||||
if self._fretboard is None:
|
||||
raise ValueError("Cannot resolve tones without a fretboard reference.")
|
||||
tones = []
|
||||
for pos, tone in zip(self.positions, self._fretboard.tones):
|
||||
if pos is not None:
|
||||
tones.append(tone.add(pos))
|
||||
return tones
|
||||
# Zip canonical positions with canonical open tones so they always
|
||||
# align regardless of orientation, then present in display order.
|
||||
tones = [tone.add(pos)
|
||||
for pos, tone in zip(self._positions, self._fretboard._tones)
|
||||
if pos is not None]
|
||||
return tones if self.high_to_low else list(reversed(tones))
|
||||
|
||||
def to_chord(self, fretboard=None) -> "Chord":
|
||||
"""Apply this fingering to a fretboard, returning a Chord.
|
||||
@@ -131,10 +152,9 @@ class Fingering:
|
||||
fb = fretboard or self._fretboard
|
||||
if fb is None:
|
||||
raise ValueError("No fretboard provided.")
|
||||
tones = []
|
||||
for pos, tone in zip(self.positions, fb.tones):
|
||||
if pos is not None:
|
||||
tones.append(tone.add(pos))
|
||||
tones = [tone.add(pos)
|
||||
for pos, tone in zip(self._positions, fb._tones)
|
||||
if pos is not None]
|
||||
return Chord(tones=tones)
|
||||
|
||||
def identify(self) -> Optional[str]:
|
||||
@@ -151,12 +171,12 @@ class Fingering:
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> print(fb.chord("C").tab())
|
||||
C
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
E|--x--
|
||||
A|--3--
|
||||
E|--0--
|
||||
D|--2--
|
||||
G|--0--
|
||||
B|--1--
|
||||
e|--0--
|
||||
"""
|
||||
if self._fretboard is None:
|
||||
raise ValueError("Cannot render tab without a fretboard reference.")
|
||||
@@ -306,7 +326,7 @@ class NamedChord:
|
||||
return tuple(fingerings)
|
||||
|
||||
fingering = []
|
||||
for i, tone in enumerate(fretboard.tones):
|
||||
for i, tone in enumerate(fretboard._tones):
|
||||
frets = find_fingerings(tone)
|
||||
# Always allow muting as an option
|
||||
if frets:
|
||||
@@ -335,8 +355,13 @@ class NamedChord:
|
||||
return tuple(itertools.product(*self._possible_fingerings(fretboard=fretboard)))
|
||||
|
||||
def _cache_key(self, fretboard):
|
||||
"""Return a hashable key for memoization."""
|
||||
return (self.name, tuple(t.full_name for t in fretboard.tones))
|
||||
"""Return a hashable key for memoization.
|
||||
|
||||
Keyed on canonical tones plus orientation so the two display
|
||||
orderings of the same board don't collide in the result caches.
|
||||
"""
|
||||
return (self.name, tuple(t.full_name for t in fretboard._tones),
|
||||
fretboard.high_to_low)
|
||||
|
||||
def fingering(self, *, fretboard, multiple=False):
|
||||
# Check cache first
|
||||
@@ -348,19 +373,22 @@ class NamedChord:
|
||||
if key in _fingering_cache:
|
||||
return _fingering_cache[key]
|
||||
|
||||
# Check for curated guitar chord overrides in standard tuning
|
||||
tuning = tuple(t.full_name for t in fretboard.tones)
|
||||
# Check for curated guitar chord overrides in standard tuning.
|
||||
# Overrides are written in canonical (high-to-low) order.
|
||||
tuning = tuple(t.full_name for t in fretboard._tones)
|
||||
if tuning == STANDARD_GUITAR_TUNING and self.name in GUITAR_OVERRIDES:
|
||||
string_names = tuple(t.name for t in fretboard.tones)
|
||||
string_names = tuple(t.name for t in fretboard._tones)
|
||||
override = GUITAR_OVERRIDES[self.name]
|
||||
if not multiple:
|
||||
result = Fingering(self.fix_fingering(override), string_names, fretboard=fretboard)
|
||||
result = Fingering(self.fix_fingering(override), string_names,
|
||||
fretboard=fretboard, high_to_low=fretboard.high_to_low)
|
||||
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_cache.clear()
|
||||
_fingering_cache[key] = result
|
||||
return result
|
||||
else:
|
||||
result = (Fingering(self.fix_fingering(override), string_names, fretboard=fretboard),)
|
||||
result = (Fingering(self.fix_fingering(override), string_names,
|
||||
fretboard=fretboard, high_to_low=fretboard.high_to_low),)
|
||||
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_multi_cache.clear()
|
||||
_fingering_multi_cache[key] = result
|
||||
@@ -390,7 +418,7 @@ class NamedChord:
|
||||
sounding_names = set()
|
||||
for i, f in enumerate(fingering):
|
||||
if f != -1:
|
||||
sounding_names.add(fretboard.tones[i].add(f).name)
|
||||
sounding_names.add(fretboard._tones[i].add(f).name)
|
||||
required = set(t.name for t in self.acceptable_tones)
|
||||
missing = required - sounding_names
|
||||
score -= len(missing) * 5.0
|
||||
@@ -433,12 +461,13 @@ class NamedChord:
|
||||
if fingers_needed > 4:
|
||||
score -= (fingers_needed - 4) * 5.0
|
||||
|
||||
# Reward root in bass — the lowest sounding string
|
||||
# Reward root in bass — the lowest sounding string. `fingering`
|
||||
# is canonical (high-to-low), so the last index is the bass.
|
||||
for i in range(len(fingering) - 1, -1, -1):
|
||||
f = fingering[i]
|
||||
if f == -1:
|
||||
continue
|
||||
bass_tone = fretboard.tones[i].add(f)
|
||||
bass_tone = fretboard._tones[i].add(f)
|
||||
if bass_tone.name == self.tone.name:
|
||||
score += 4.0
|
||||
else:
|
||||
@@ -467,17 +496,20 @@ class NamedChord:
|
||||
if s == max_score:
|
||||
yield possible_fingering
|
||||
|
||||
string_names = tuple(t.name for t in fretboard.tones)
|
||||
string_names = tuple(t.name for t in fretboard._tones)
|
||||
best_fingerings = tuple([g for g in gen()])
|
||||
if not multiple:
|
||||
result = Fingering(self.fix_fingering(best_fingerings[0]), string_names, fretboard=fretboard)
|
||||
result = Fingering(self.fix_fingering(best_fingerings[0]), string_names,
|
||||
fretboard=fretboard, high_to_low=fretboard.high_to_low)
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_cache.clear()
|
||||
_fingering_cache[key] = result
|
||||
return result
|
||||
else:
|
||||
result = tuple([Fingering(self.fix_fingering(f), string_names, fretboard=fretboard) for f in best_fingerings])
|
||||
result = tuple([Fingering(self.fix_fingering(f), string_names,
|
||||
fretboard=fretboard, high_to_low=fretboard.high_to_low)
|
||||
for f in best_fingerings])
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_multi_cache.clear()
|
||||
@@ -491,18 +523,21 @@ class NamedChord:
|
||||
|
||||
>>> print(CHARTS["western"]["C"].tab(fretboard=Fretboard.guitar()))
|
||||
C
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
E|--x--
|
||||
A|--3--
|
||||
E|--0--
|
||||
D|--2--
|
||||
G|--0--
|
||||
B|--1--
|
||||
e|--0--
|
||||
"""
|
||||
fingering = self.fingering(fretboard=fretboard)
|
||||
string_names = [t.name for t in fretboard.tones]
|
||||
# Use the fingering's oriented, disambiguated string names/positions
|
||||
# so the tab honors the fretboard's orientation.
|
||||
string_names = fingering.string_names
|
||||
positions = fingering.positions
|
||||
lines = [self.name]
|
||||
max_name = max(len(n) for n in string_names)
|
||||
for i, (name, fret) in enumerate(zip(string_names, fingering)):
|
||||
for name, fret in zip(string_names, positions):
|
||||
fret_str = "x" if fret is None else str(fret)
|
||||
lines.append(f"{name:>{max_name}}|--{fret_str}--")
|
||||
return "\n".join(lines)
|
||||
|
||||
+154
-165
@@ -1352,14 +1352,63 @@ class Chord:
|
||||
|
||||
|
||||
class Fretboard:
|
||||
def __init__(self, *, tones: list[Tone]) -> None:
|
||||
def __init__(self, *, tones: list[Tone], high_to_low: bool = False,
|
||||
_canonical: bool = False) -> None:
|
||||
"""Initialize a Fretboard from a list of open-string Tone objects.
|
||||
|
||||
Args:
|
||||
tones: A list of :class:`Tone` instances representing the
|
||||
open strings (high to low).
|
||||
open strings. By default these are read **low to high**
|
||||
(low string first) — pass ``high_to_low=True`` if your
|
||||
list runs high to low instead.
|
||||
high_to_low: Orientation of this fretboard. When ``False``
|
||||
(the default since v0.43.0), strings and fingerings read
|
||||
low to high; when ``True``, they read high to low (the
|
||||
pre-0.43 behavior).
|
||||
_canonical: Internal flag — when ``True``, *tones* are already
|
||||
in canonical (high-to-low) order and are stored as-is.
|
||||
Used by the instrument presets.
|
||||
"""
|
||||
self.tones = tones
|
||||
self.high_to_low = high_to_low
|
||||
# Internally we always store strings high-to-low; this keeps the
|
||||
# fingering scorer and chord-override tables (which assume that
|
||||
# order) untouched. User-facing access is re-oriented on the way out.
|
||||
if _canonical or high_to_low:
|
||||
self._tones = list(tones)
|
||||
else:
|
||||
self._tones = list(reversed(tones))
|
||||
|
||||
def _orient(self, seq):
|
||||
"""Re-orient a canonical (high-to-low) sequence for display.
|
||||
|
||||
Returns *seq* unchanged when this board reads high-to-low, or
|
||||
reversed when it reads low-to-high. Self-inverse, so it also maps
|
||||
user-supplied (oriented) input back to canonical order.
|
||||
"""
|
||||
return list(seq) if self.high_to_low else list(reversed(seq))
|
||||
|
||||
@property
|
||||
def tones(self) -> list[Tone]:
|
||||
"""The open-string tones in this board's orientation.
|
||||
|
||||
Low-to-high by default; high-to-low when ``high_to_low=True``.
|
||||
"""
|
||||
return self._orient(self._tones)
|
||||
|
||||
@classmethod
|
||||
def _from_canonical(cls, tone_strings, high_to_low: bool = False) -> Fretboard:
|
||||
"""Build a board from canonical (high-to-low) tone-name strings.
|
||||
|
||||
Used by the instrument presets, whose tunings are written in the
|
||||
conventional high-to-low order. *high_to_low* sets only the
|
||||
board's display orientation.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(
|
||||
tones=[Tone.from_string(t, system="western") for t in tone_strings],
|
||||
high_to_low=high_to_low,
|
||||
_canonical=True,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
l = tuple([tone.full_name for tone in self.tones])
|
||||
@@ -1391,7 +1440,11 @@ class Fretboard:
|
||||
Returns:
|
||||
A new Fretboard with all strings raised by ``fret`` semitones.
|
||||
"""
|
||||
return Fretboard(tones=[t.add(fret) for t in self.tones])
|
||||
return Fretboard(
|
||||
tones=[t.add(fret) for t in self._tones],
|
||||
high_to_low=self.high_to_low,
|
||||
_canonical=True,
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[Tone]:
|
||||
"""Iterate over the open-string tones of this fretboard."""
|
||||
@@ -1399,7 +1452,7 @@ class Fretboard:
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of strings on this fretboard."""
|
||||
return len(self.tones)
|
||||
return len(self._tones)
|
||||
|
||||
INSTRUMENTS = [
|
||||
"guitar", "twelve_string", "bass", "ukulele",
|
||||
@@ -1423,84 +1476,76 @@ class Fretboard:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def guitar(cls, tuning: Union[str, tuple[str, ...]] = "standard", capo: int = 0) -> Fretboard:
|
||||
def guitar(cls, tuning: Union[str, tuple[str, ...]] = "standard", capo: int = 0,
|
||||
high_to_low: bool = False) -> Fretboard:
|
||||
"""Guitar with the given tuning and optional capo.
|
||||
|
||||
Args:
|
||||
tuning: Tuning name or tuple of tone strings (high to low).
|
||||
tuning: Tuning name, or a tuple of tone strings. A custom
|
||||
tuple is read **low to high** by default (pass
|
||||
``high_to_low=True`` to give it high to low instead).
|
||||
Built-in tunings: standard, drop d, open g, open d,
|
||||
open e, open a, dadgad, half step down.
|
||||
capo: Fret number for the capo (0 = no capo). Raises all
|
||||
strings by this many semitones.
|
||||
high_to_low: When ``True``, the resulting board reads high to
|
||||
low (pre-0.43 behavior); otherwise low to high.
|
||||
"""
|
||||
from .tones import Tone
|
||||
if isinstance(tuning, str):
|
||||
tuning = cls.TUNINGS[tuning]
|
||||
fb = cls(tones=[Tone.from_string(t, system="western") for t in tuning])
|
||||
# Built-in tunings are defined canonically (high to low).
|
||||
canonical = [Tone.from_string(t, system="western") for t in cls.TUNINGS[tuning]]
|
||||
fb = cls(tones=canonical, high_to_low=high_to_low, _canonical=True)
|
||||
else:
|
||||
# A user-supplied tuple is in the board's orientation.
|
||||
fb = cls(tones=[Tone.from_string(t, system="western") for t in tuning],
|
||||
high_to_low=high_to_low)
|
||||
if capo:
|
||||
fb = fb.capo(capo)
|
||||
return fb
|
||||
|
||||
@classmethod
|
||||
def bass(cls, five_string: bool = False) -> Fretboard:
|
||||
def bass(cls, five_string: bool = False, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard bass guitar tuning.
|
||||
|
||||
Args:
|
||||
five_string: If True, adds a low B string (B0).
|
||||
high_to_low: When ``True``, the board reads high to low.
|
||||
"""
|
||||
from .tones import Tone
|
||||
strings = ["G2", "D2", "A1", "E1"]
|
||||
if five_string:
|
||||
strings.append("B0")
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def ukulele(cls) -> Fretboard:
|
||||
def ukulele(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard ukulele tuning (A4 E4 C4 G4).
|
||||
|
||||
Re-entrant tuning: the G4 string is higher than C4.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("C4", system="western"),
|
||||
Tone.from_string("G4", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "E4", "C4", "G4"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def mandolin(cls) -> Fretboard:
|
||||
def mandolin(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard mandolin tuning (E5 A4 D4 G3).
|
||||
|
||||
Tuned in fifths, same as a violin but one octave relationship.
|
||||
Strings are typically doubled (paired courses).
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("E5", system="western"),
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["E5", "A4", "D4", "G3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def mandola(cls) -> Fretboard:
|
||||
def mandola(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard mandola tuning (A4 D4 G3 C3).
|
||||
|
||||
The mandola (or tenor mandola) is to the mandolin what the
|
||||
viola is to the violin — a fifth lower, with a warmer,
|
||||
darker tone. Tuned in fifths like all the mandolin family.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
Tone.from_string("C3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "D4", "G3", "C3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def octave_mandolin(cls) -> Fretboard:
|
||||
def octave_mandolin(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Octave mandolin tuning (E4 A3 D3 G2).
|
||||
|
||||
Also called the octave mandola in European terminology.
|
||||
@@ -1508,84 +1553,57 @@ class Fretboard:
|
||||
family's cello-to-violin relationship. Popular in Irish
|
||||
and Celtic folk music.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("A3", system="western"),
|
||||
Tone.from_string("D3", system="western"),
|
||||
Tone.from_string("G2", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["E4", "A3", "D3", "G2"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def mandocello(cls) -> Fretboard:
|
||||
def mandocello(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Mandocello tuning (A3 D3 G2 C2).
|
||||
|
||||
The bass of the mandolin family. Tuned like a cello — an
|
||||
octave below the mandola. Rare but beautiful; used in
|
||||
mandolin orchestras.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A3", system="western"),
|
||||
Tone.from_string("D3", system="western"),
|
||||
Tone.from_string("G2", system="western"),
|
||||
Tone.from_string("C2", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A3", "D3", "G2", "C2"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def violin(cls) -> Fretboard:
|
||||
def violin(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard violin tuning (E5 A4 D4 G3).
|
||||
|
||||
Tuned in perfect fifths. The violin has no frets — intonation
|
||||
is continuous, allowing vibrato and microtonal inflections
|
||||
not possible on fretted instruments.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("E5", system="western"),
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["E5", "A4", "D4", "G3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def viola(cls) -> Fretboard:
|
||||
def viola(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard viola tuning (A4 D4 G3 C3).
|
||||
|
||||
A perfect fifth below the violin. The viola's darker, warmer
|
||||
tone comes from its larger body and lower register.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
Tone.from_string("C3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "D4", "G3", "C3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def cello(cls) -> Fretboard:
|
||||
def cello(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard cello tuning (A3 D3 G2 C2).
|
||||
|
||||
An octave below the viola. Tuned in fifths. The cello spans
|
||||
the range of the human voice — tenor through bass.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A3", system="western"),
|
||||
Tone.from_string("D3", system="western"),
|
||||
Tone.from_string("G2", system="western"),
|
||||
Tone.from_string("C2", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A3", "D3", "G2", "C2"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def banjo(cls, tuning: Union[str, tuple[str, ...]] = "open g") -> Fretboard:
|
||||
def banjo(cls, tuning: Union[str, tuple[str, ...]] = "open g",
|
||||
high_to_low: bool = False) -> Fretboard:
|
||||
"""Banjo with the given tuning.
|
||||
|
||||
Args:
|
||||
tuning: ``"open g"`` (default, bluegrass) or ``"open d"``
|
||||
(old-time, clawhammer). The 5th string is a high
|
||||
drone — a defining feature of the banjo sound.
|
||||
drone — a defining feature of the banjo sound. A custom
|
||||
tuple is read low to high unless ``high_to_low=True``.
|
||||
high_to_low: When ``True``, the board reads high to low.
|
||||
|
||||
Standard open G: G4 D3 G3 B3 D4 (5th string is the short
|
||||
high G4 drone).
|
||||
@@ -1597,11 +1615,12 @@ class Fretboard:
|
||||
"double c": ("D4", "C4", "G3", "C3", "G4"),
|
||||
}
|
||||
if isinstance(tuning, str):
|
||||
tuning = tunings[tuning]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in tuning])
|
||||
return cls._from_canonical(tunings[tuning], high_to_low)
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in tuning],
|
||||
high_to_low=high_to_low)
|
||||
|
||||
@classmethod
|
||||
def double_bass(cls) -> Fretboard:
|
||||
def double_bass(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard double bass (upright bass) tuning (G2 D2 A1 E1).
|
||||
|
||||
The largest and lowest-pitched bowed string instrument in the
|
||||
@@ -1611,16 +1630,10 @@ class Fretboard:
|
||||
|
||||
The 5-string double bass adds a low B0 or C1.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("G2", system="western"),
|
||||
Tone.from_string("D2", system="western"),
|
||||
Tone.from_string("A1", system="western"),
|
||||
Tone.from_string("E1", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["G2", "D2", "A1", "E1"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def harp(cls) -> Fretboard:
|
||||
def harp(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Concert harp strings — 47 strings spanning C1 to G7.
|
||||
|
||||
The pedal harp has 7 strings per octave (one per note name),
|
||||
@@ -1630,7 +1643,6 @@ class Fretboard:
|
||||
This returns the full set of 47 strings in the default
|
||||
Cb (enharmonic B) tuning.
|
||||
"""
|
||||
from .tones import Tone
|
||||
# 47 strings: C1 to G7, one per diatonic note
|
||||
notes = ["C", "D", "E", "F", "G", "A", "B"]
|
||||
strings = []
|
||||
@@ -1643,30 +1655,33 @@ class Fretboard:
|
||||
else:
|
||||
continue
|
||||
break
|
||||
# Harp strings are high to low
|
||||
# Canonical (high to low)
|
||||
strings.reverse()
|
||||
return cls(tones=[Tone.from_string(s, system="western") for s in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def pedal_steel(cls) -> Fretboard:
|
||||
def pedal_steel(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Pedal steel guitar — E9 Nashville tuning (10 strings).
|
||||
|
||||
The standard tuning for country music. The pedal steel has
|
||||
foot pedals and knee levers that change string pitches during
|
||||
play, enabling its signature swooping, crying sound.
|
||||
"""
|
||||
from .tones import Tone
|
||||
# E9 Nashville tuning (high to low)
|
||||
# E9 Nashville tuning (canonical: high to low)
|
||||
strings = ["F#4", "D#4", "G#3", "E3", "B3", "G#3",
|
||||
"F#3", "E3", "D3", "B2"]
|
||||
return cls(tones=[Tone.from_string(s, system="western") for s in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def bouzouki(cls, variant: Union[str, tuple[str, ...]] = "irish") -> Fretboard:
|
||||
def bouzouki(cls, variant: Union[str, tuple[str, ...]] = "irish",
|
||||
high_to_low: bool = False) -> Fretboard:
|
||||
"""Bouzouki tuning.
|
||||
|
||||
Args:
|
||||
variant: ``"irish"`` (default, GDAD) or ``"greek"`` (CFAD).
|
||||
A custom tuple is read low to high unless
|
||||
``high_to_low=True``.
|
||||
high_to_low: When ``True``, the board reads high to low.
|
||||
|
||||
The Irish bouzouki is a staple of Celtic music, usually tuned
|
||||
in unison or octave pairs. The Greek bouzouki traditionally
|
||||
@@ -1678,11 +1693,12 @@ class Fretboard:
|
||||
"greek": ("D4", "A3", "F3", "C3"),
|
||||
}
|
||||
if isinstance(variant, str):
|
||||
variant = tunings[variant]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in variant])
|
||||
return cls._from_canonical(tunings[variant], high_to_low)
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in variant],
|
||||
high_to_low=high_to_low)
|
||||
|
||||
@classmethod
|
||||
def oud(cls) -> Fretboard:
|
||||
def oud(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard Arabic oud tuning (C4 G3 D3 A2 G2 C2).
|
||||
|
||||
The oud is the ancestor of the European lute and the defining
|
||||
@@ -1691,12 +1707,11 @@ class Fretboard:
|
||||
essential to maqam performance. 6 courses (11 strings),
|
||||
typically tuned in fourths.
|
||||
"""
|
||||
from .tones import Tone
|
||||
strings = ["C4", "G3", "D3", "A2", "G2", "C2"]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def sitar(cls) -> Fretboard:
|
||||
def sitar(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Sitar main playing strings (approximation).
|
||||
|
||||
The sitar typically has 6-7 main strings and 11-13 sympathetic
|
||||
@@ -1706,14 +1721,13 @@ class Fretboard:
|
||||
Main strings: Sa Sa Pa Sa Re Sa Ma (approximated in 12-TET).
|
||||
Represented here as the most common Ravi Shankar school tuning.
|
||||
"""
|
||||
from .tones import Tone
|
||||
# Common Ravi Shankar tuning mapped to Western notes
|
||||
# (sitar is tuned relative to Sa, typically C# or D)
|
||||
strings = ["C4", "C3", "G3", "C3", "D3", "C2", "F2"]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def shamisen(cls) -> Fretboard:
|
||||
def shamisen(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard shamisen tuning — honchoshi (C4 G3 C3).
|
||||
|
||||
The shamisen is a 3-stringed Japanese instrument played with
|
||||
@@ -1723,15 +1737,10 @@ class Fretboard:
|
||||
- niagari (二上り): root-5th-2nd (raises 2nd string)
|
||||
- sansagari (三下り): root-5th-b7th (lowers 3rd string)
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("C4", system="western"),
|
||||
Tone.from_string("G3", system="western"),
|
||||
Tone.from_string("C3", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["C4", "G3", "C3"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def erhu(cls) -> Fretboard:
|
||||
def erhu(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard erhu tuning (A4 D4).
|
||||
|
||||
The erhu is a 2-stringed Chinese bowed instrument with a
|
||||
@@ -1739,14 +1748,10 @@ class Fretboard:
|
||||
— the player presses the strings without touching the neck,
|
||||
allowing continuous pitch bending.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("D4", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "D4"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def charango(cls) -> Fretboard:
|
||||
def charango(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard charango tuning (E5 A4 E5 C5 G4).
|
||||
|
||||
A small Andean stringed instrument, traditionally made from
|
||||
@@ -1754,54 +1759,38 @@ class Fretboard:
|
||||
— the 3rd course (E5) is the highest pitched, creating the
|
||||
charango's bright, sparkling sound.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("E5", system="western"),
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("E5", system="western"),
|
||||
Tone.from_string("C5", system="western"),
|
||||
Tone.from_string("G4", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["E5", "A4", "E5", "C5", "G4"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def pipa(cls) -> Fretboard:
|
||||
def pipa(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard pipa tuning (D4 A3 E3 A2).
|
||||
|
||||
The pipa is a 4-stringed Chinese lute with a pear-shaped
|
||||
body, dating back over 2000 years. Known for its percussive
|
||||
attack and rapid tremolo technique.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("D4", system="western"),
|
||||
Tone.from_string("A3", system="western"),
|
||||
Tone.from_string("E3", system="western"),
|
||||
Tone.from_string("A2", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["D4", "A3", "E3", "A2"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def balalaika(cls) -> Fretboard:
|
||||
def balalaika(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Standard balalaika prima tuning (A4 E4 E4).
|
||||
|
||||
The Russian balalaika has a distinctive triangular body and
|
||||
3 strings. The two lower strings are tuned in unison — a
|
||||
unique feature that gives it a natural chorus effect.
|
||||
"""
|
||||
from .tones import Tone
|
||||
return cls(tones=[
|
||||
Tone.from_string("A4", system="western"),
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("E4", system="western"),
|
||||
])
|
||||
return cls._from_canonical(["A4", "E4", "E4"], high_to_low)
|
||||
|
||||
@classmethod
|
||||
def keyboard(cls, keys: int = 88, start: str = "A0") -> Fretboard:
|
||||
def keyboard(cls, keys: int = 88, start: str = "A0",
|
||||
high_to_low: bool = False) -> Fretboard:
|
||||
"""Piano or keyboard with the given number of keys.
|
||||
|
||||
Args:
|
||||
keys: Number of keys (default 88 for a full piano).
|
||||
Common sizes: 25, 37, 49, 61, 76, 88.
|
||||
start: The lowest note (default ``"A0"`` for standard piano).
|
||||
high_to_low: When ``True``, the board reads high to low.
|
||||
|
||||
A full 88-key piano spans A0 (27.5 Hz) to C8 (4186 Hz) —
|
||||
the widest range of any standard acoustic instrument.
|
||||
@@ -1815,13 +1804,12 @@ class Fretboard:
|
||||
"""
|
||||
from .tones import Tone
|
||||
start_tone = Tone.from_string(start, system="western")
|
||||
tones = []
|
||||
for i in range(keys - 1, -1, -1):
|
||||
tones.append(start_tone.add(i))
|
||||
return cls(tones=tones)
|
||||
# Built high-to-low (canonical): highest key first, down to `start`.
|
||||
tones = [start_tone.add(i) for i in range(keys - 1, -1, -1)]
|
||||
return cls(tones=tones, high_to_low=high_to_low, _canonical=True)
|
||||
|
||||
@classmethod
|
||||
def lute(cls) -> Fretboard:
|
||||
def lute(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""Renaissance lute in G tuning (6 courses).
|
||||
|
||||
The European lute was the dominant instrument of the
|
||||
@@ -1829,21 +1817,19 @@ class Fretboard:
|
||||
a major third between the 3rd and 4th courses — the
|
||||
same intervallic pattern as a modern guitar.
|
||||
"""
|
||||
from .tones import Tone
|
||||
strings = ["G4", "D4", "A3", "F3", "C3", "G2"]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
@classmethod
|
||||
def twelve_string(cls) -> Fretboard:
|
||||
def twelve_string(cls, high_to_low: bool = False) -> Fretboard:
|
||||
"""12-string guitar in standard tuning.
|
||||
|
||||
The lower 4 courses are doubled at the octave; the upper 2
|
||||
are doubled in unison. This creates the characteristic
|
||||
shimmering, chorus-like sound.
|
||||
|
||||
Represented as 12 strings (high to low, pairs together).
|
||||
Represented as 12 strings (canonical: high to low, pairs together).
|
||||
"""
|
||||
from .tones import Tone
|
||||
strings = [
|
||||
"E4", "E4", # 1st course (unison)
|
||||
"B3", "B3", # 2nd course (unison)
|
||||
@@ -1852,7 +1838,7 @@ class Fretboard:
|
||||
"A3", "A2", # 5th course (octave)
|
||||
"E3", "E2", # 6th course (octave)
|
||||
]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
|
||||
return cls._from_canonical(strings, high_to_low)
|
||||
|
||||
def scale_diagram(self, scale, frets: int = 12, chord=None) -> str:
|
||||
"""Render an ASCII diagram showing where scale notes fall on the neck.
|
||||
@@ -1927,7 +1913,7 @@ class Fretboard:
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> 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)
|
||||
"""
|
||||
from .charts import CHARTS
|
||||
return CHARTS[system][name].fingering(fretboard=self)
|
||||
@@ -1945,7 +1931,7 @@ class Fretboard:
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb["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)
|
||||
"""
|
||||
return self.chord(name)
|
||||
|
||||
@@ -1964,12 +1950,12 @@ class Fretboard:
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> print(fb.tab("Am"))
|
||||
A minor
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--2--
|
||||
D|--2--
|
||||
E|--x--
|
||||
A|--0--
|
||||
E|--0--
|
||||
D|--2--
|
||||
G|--2--
|
||||
B|--1--
|
||||
e|--0--
|
||||
"""
|
||||
return self.chord(name, system=system).tab()
|
||||
|
||||
@@ -1984,7 +1970,7 @@ class Fretboard:
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> chart = fb.chart()
|
||||
>>> chart["Am7"]
|
||||
Fingering(e=0, B=1, G=0, D=2, A=0, E=0)
|
||||
Fingering(E=0, A=0, D=2, G=0, B=1, e=0)
|
||||
"""
|
||||
from .charts import charts_for_fretboard, CHARTS
|
||||
return charts_for_fretboard(chart=CHARTS[system], fretboard=self)
|
||||
@@ -2009,13 +1995,16 @@ class Fretboard:
|
||||
"""
|
||||
from .charts import Fingering
|
||||
|
||||
if not len(positions) == len(self.tones):
|
||||
if not len(positions) == len(self._tones):
|
||||
raise ValueError(
|
||||
"The number of positions must match the number of tones (strings)."
|
||||
)
|
||||
|
||||
string_names = tuple(t.name for t in self.tones)
|
||||
return Fingering(positions, string_names, fretboard=self)
|
||||
# Positions arrive in this board's orientation; canonicalise them
|
||||
# (high-to-low) to match the internal tone order Fingering expects.
|
||||
string_names = tuple(t.name for t in self._tones)
|
||||
return Fingering(self._orient(positions), string_names, fretboard=self,
|
||||
high_to_low=self.high_to_low)
|
||||
|
||||
|
||||
def analyze_progression(chords: list[Chord], key: str = "C", mode: str = "major") -> list[str | None]:
|
||||
|
||||
+9
-6
@@ -3706,17 +3706,19 @@ class Part:
|
||||
fingering = self._fretboard.chord(chord_name)
|
||||
|
||||
# Get the sounding tones (skips muted strings)
|
||||
tones = fingering.tones # list of Tone objects, high to low
|
||||
tones = fingering.tones
|
||||
|
||||
if not tones:
|
||||
self.rest(duration)
|
||||
return self
|
||||
|
||||
# Order: down strum = low to high (reverse since tones are high-to-low)
|
||||
# Sort by pitch so strum direction is correct regardless of the
|
||||
# fingering's display orientation: down = low to high, up = high to low.
|
||||
low_to_high = sorted(tones, key=lambda t: t.midi)
|
||||
if direction == "down":
|
||||
strum_tones = list(reversed(tones))
|
||||
strum_tones = low_to_high
|
||||
else:
|
||||
strum_tones = list(tones)
|
||||
strum_tones = list(reversed(low_to_high))
|
||||
|
||||
if hasattr(duration, 'value'):
|
||||
total_beats = duration.value
|
||||
@@ -3826,8 +3828,9 @@ class Part:
|
||||
open_midis = list(self._TAB_TUNINGS[tuning])
|
||||
labels = list(self._TAB_LABELS[tuning])
|
||||
elif hasattr(tuning, "tones"):
|
||||
# Fretboard object — tones are high-to-low, reverse for low-to-high
|
||||
fb_tones = list(reversed(tuning.tones))
|
||||
# Fretboard object — sort by pitch so we get low-to-high
|
||||
# regardless of the board's display orientation.
|
||||
fb_tones = sorted(tuning.tones, key=lambda t: t.midi)
|
||||
open_midis = [t.midi for t in fb_tones]
|
||||
labels = [t.name if len(t.name) <= 2 else t.name[0] for t in fb_tones]
|
||||
else:
|
||||
|
||||
+100
-33
@@ -482,7 +482,8 @@ def test_fretboard_creation():
|
||||
Tone(name="A", octave=2),
|
||||
Tone(name="E", octave=2),
|
||||
]
|
||||
fretboard = Fretboard(tones=standard_tuning)
|
||||
# Literal is written high-to-low, so declare that orientation.
|
||||
fretboard = Fretboard(tones=standard_tuning, high_to_low=True)
|
||||
assert len(fretboard.tones) == 6
|
||||
assert fretboard.tones[0].full_name == "E4"
|
||||
assert fretboard.tones[-1].full_name == "E2"
|
||||
@@ -505,7 +506,8 @@ def guitar_fretboard():
|
||||
Tone.from_string("A2"),
|
||||
Tone.from_string("E2"),
|
||||
]
|
||||
return Fretboard(tones=tuning)
|
||||
# Literal is written high-to-low, so declare that orientation.
|
||||
return Fretboard(tones=tuning, high_to_low=True)
|
||||
|
||||
|
||||
def test_chord_fingering_c(guitar_fretboard):
|
||||
@@ -1767,28 +1769,32 @@ def test_chord_contains_tone():
|
||||
def test_fretboard_guitar():
|
||||
fb = Fretboard.guitar()
|
||||
assert len(fb) == 6
|
||||
# Low-to-high by default (v0.43.0).
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["E", "B", "G", "D", "A", "E"]
|
||||
assert names == ["E", "A", "D", "G", "B", "E"]
|
||||
# high_to_low=True restores the pre-0.43 order.
|
||||
assert [t.name for t in Fretboard.guitar(high_to_low=True)] == \
|
||||
["E", "B", "G", "D", "A", "E"]
|
||||
|
||||
|
||||
def test_fretboard_guitar_octaves():
|
||||
fb = Fretboard.guitar()
|
||||
octaves = [t.octave for t in fb]
|
||||
assert octaves == [4, 3, 3, 3, 2, 2]
|
||||
assert octaves == [2, 2, 3, 3, 3, 4]
|
||||
|
||||
|
||||
def test_fretboard_bass():
|
||||
fb = Fretboard.bass()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["G", "D", "A", "E"]
|
||||
assert names == ["E", "A", "D", "G"]
|
||||
|
||||
|
||||
def test_fretboard_ukulele():
|
||||
fb = Fretboard.ukulele()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["A", "E", "C", "G"]
|
||||
assert names == ["G", "C", "E", "A"]
|
||||
|
||||
|
||||
def test_fretboard_iter():
|
||||
@@ -1821,8 +1827,9 @@ def test_fretboard_ukulele_fingerings():
|
||||
def test_fretboard_guitar_drop_d():
|
||||
fb = Fretboard.guitar("drop d")
|
||||
assert len(fb) == 6
|
||||
assert fb.tones[-1].name == "D"
|
||||
assert fb.tones[-1].octave == 2
|
||||
# Low-to-high: the dropped low D is now the first string.
|
||||
assert fb.tones[0].name == "D"
|
||||
assert fb.tones[0].octave == 2
|
||||
|
||||
|
||||
def test_fretboard_guitar_open_g():
|
||||
@@ -1840,7 +1847,8 @@ def test_fretboard_guitar_custom_tuple():
|
||||
def test_fretboard_bass_five_string():
|
||||
fb = Fretboard.bass(five_string=True)
|
||||
assert len(fb) == 5
|
||||
assert fb.tones[-1].name == "B"
|
||||
# Low-to-high: the added low B is the first string.
|
||||
assert fb.tones[0].name == "B"
|
||||
|
||||
|
||||
def test_fretboard_tunings_dict():
|
||||
@@ -1852,36 +1860,37 @@ def test_fretboard_tunings_dict():
|
||||
def test_fretboard_mandolin():
|
||||
fb = Fretboard.mandolin()
|
||||
assert len(fb) == 4
|
||||
assert fb.tones[0].name == "E"
|
||||
assert fb.tones[-1].name == "G"
|
||||
# Low-to-high.
|
||||
assert fb.tones[0].name == "G"
|
||||
assert fb.tones[-1].name == "E"
|
||||
|
||||
|
||||
def test_fretboard_violin():
|
||||
fb = Fretboard.violin()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["E", "A", "D", "G"]
|
||||
assert names == ["G", "D", "A", "E"]
|
||||
|
||||
|
||||
def test_fretboard_viola():
|
||||
fb = Fretboard.viola()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["A", "D", "G", "C"]
|
||||
assert names == ["C", "G", "D", "A"]
|
||||
|
||||
|
||||
def test_fretboard_cello():
|
||||
fb = Fretboard.cello()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["A", "D", "G", "C"]
|
||||
assert fb.tones[0].octave == 3
|
||||
assert names == ["C", "G", "D", "A"]
|
||||
assert fb.tones[0].octave == 2
|
||||
|
||||
|
||||
def test_fretboard_banjo():
|
||||
fb = Fretboard.banjo()
|
||||
assert len(fb) == 5
|
||||
assert fb.tones[-1].name == "G" # high drone string
|
||||
assert fb.tones[0].name == "G" # high drone string (now first, low-to-high)
|
||||
|
||||
|
||||
def test_fretboard_banjo_open_d():
|
||||
@@ -1898,46 +1907,49 @@ def test_fretboard_violin_tuned_in_fifths():
|
||||
"""Violin strings should be a perfect 5th apart."""
|
||||
fb = Fretboard.violin()
|
||||
for i in range(len(fb.tones) - 1):
|
||||
interval = fb.tones[i] - fb.tones[i + 1]
|
||||
# Low-to-high: each next string is a 5th higher.
|
||||
interval = fb.tones[i + 1] - fb.tones[i]
|
||||
assert interval == 7, f"Strings {i} and {i+1} not a 5th apart"
|
||||
|
||||
|
||||
def test_fretboard_octave_mandolin():
|
||||
fb = Fretboard.octave_mandolin()
|
||||
assert len(fb) == 4
|
||||
assert fb.tones[0].name == "E"
|
||||
assert fb.tones[0].octave == 4
|
||||
assert fb.tones[-1].name == "E"
|
||||
assert fb.tones[-1].octave == 4
|
||||
|
||||
|
||||
def test_fretboard_mandocello():
|
||||
fb = Fretboard.mandocello()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["A", "D", "G", "C"]
|
||||
assert fb.tones[0].octave == 3
|
||||
assert names == ["C", "G", "D", "A"]
|
||||
assert fb.tones[0].octave == 2
|
||||
|
||||
|
||||
def test_fretboard_double_bass():
|
||||
fb = Fretboard.double_bass()
|
||||
assert len(fb) == 4
|
||||
names = [t.name for t in fb]
|
||||
assert names == ["G", "D", "A", "E"]
|
||||
assert names == ["E", "A", "D", "G"]
|
||||
|
||||
|
||||
def test_fretboard_double_bass_tuned_in_fourths():
|
||||
fb = Fretboard.double_bass()
|
||||
for i in range(len(fb.tones) - 1):
|
||||
interval = fb.tones[i] - fb.tones[i + 1]
|
||||
# Low-to-high: each next string is a 4th higher.
|
||||
interval = fb.tones[i + 1] - fb.tones[i]
|
||||
assert interval == 5, f"Strings {i} and {i+1} not a 4th apart"
|
||||
|
||||
|
||||
def test_fretboard_harp():
|
||||
fb = Fretboard.harp()
|
||||
assert len(fb) == 47
|
||||
assert fb.tones[0].name == "G"
|
||||
assert fb.tones[0].octave == 7
|
||||
assert fb.tones[-1].name == "C"
|
||||
assert fb.tones[-1].octave == 1
|
||||
# Low-to-high.
|
||||
assert fb.tones[0].name == "C"
|
||||
assert fb.tones[0].octave == 1
|
||||
assert fb.tones[-1].name == "G"
|
||||
assert fb.tones[-1].octave == 7
|
||||
|
||||
|
||||
def test_fretboard_pedal_steel():
|
||||
@@ -1950,7 +1962,7 @@ def test_mandolin_family_fifths():
|
||||
for name in ["mandolin", "mandola", "octave_mandolin", "mandocello"]:
|
||||
fb = getattr(Fretboard, name)()
|
||||
for i in range(len(fb.tones) - 1):
|
||||
interval = fb.tones[i] - fb.tones[i + 1]
|
||||
interval = fb.tones[i + 1] - fb.tones[i]
|
||||
assert interval == 7, f"{name} strings {i},{i+1} not a 5th apart"
|
||||
|
||||
|
||||
@@ -1982,7 +1994,7 @@ def test_fretboard_shamisen():
|
||||
def test_fretboard_erhu():
|
||||
fb = Fretboard.erhu()
|
||||
assert len(fb) == 2
|
||||
assert fb.tones[0] - fb.tones[1] == 7 # tuned in 5ths
|
||||
assert fb.tones[1] - fb.tones[0] == 7 # tuned in 5ths (low-to-high)
|
||||
|
||||
|
||||
def test_fretboard_bouzouki_irish():
|
||||
@@ -2003,8 +2015,8 @@ def test_fretboard_charango():
|
||||
def test_fretboard_balalaika():
|
||||
fb = Fretboard.balalaika()
|
||||
assert len(fb) == 3
|
||||
# Two unison strings
|
||||
assert fb.tones[1].name == fb.tones[2].name
|
||||
# Two unison strings (now the lowest two, low-to-high)
|
||||
assert fb.tones[0].name == fb.tones[1].name
|
||||
|
||||
|
||||
def test_fretboard_lute():
|
||||
@@ -2030,8 +2042,9 @@ def test_keyboard_88():
|
||||
def test_keyboard_25():
|
||||
kb = Fretboard.keyboard(25, "C3")
|
||||
assert len(kb) == 25
|
||||
assert kb.tones[-1].name == "C"
|
||||
assert kb.tones[-1].octave == 3
|
||||
# Low-to-high: the start note is now the first key.
|
||||
assert kb.tones[0].name == "C"
|
||||
assert kb.tones[0].octave == 3
|
||||
|
||||
|
||||
def test_keyboard_custom():
|
||||
@@ -2039,6 +2052,60 @@ def test_keyboard_custom():
|
||||
assert len(kb) == 61
|
||||
|
||||
|
||||
# ── Fingering orientation (low-to-high default, v0.43.0) ─────────────────────
|
||||
|
||||
def test_chord_low_to_high_default():
|
||||
"""By default, chord fingerings read low-to-high (low E first)."""
|
||||
fb = Fretboard.guitar()
|
||||
g = fb.chord("G")
|
||||
assert g.string_names == ("E", "A", "D", "G", "B", "e")
|
||||
assert g.positions == (3, 2, 0, 0, 0, 3)
|
||||
|
||||
|
||||
def test_chord_high_to_low_opt_out():
|
||||
"""high_to_low=True restores the pre-0.43 high-to-low order."""
|
||||
fb = Fretboard.guitar(high_to_low=True)
|
||||
g = fb.chord("G")
|
||||
assert g.string_names == ("e", "B", "G", "D", "A", "E")
|
||||
assert g.positions == (3, 0, 0, 0, 2, 3)
|
||||
|
||||
|
||||
def test_orientation_is_a_reversal():
|
||||
"""The two orientations are exact reverses of each other."""
|
||||
lo = Fretboard.guitar().chord("Am7")
|
||||
hi = Fretboard.guitar(high_to_low=True).chord("Am7")
|
||||
assert lo.positions == tuple(reversed(hi.positions))
|
||||
# ...and identify to the same chord.
|
||||
assert lo.identify() == hi.identify()
|
||||
|
||||
|
||||
def test_manual_fingering_input_orientation():
|
||||
"""Manual fret positions are read in the board's orientation."""
|
||||
lo = Fretboard.guitar()
|
||||
hi = Fretboard.guitar(high_to_low=True)
|
||||
# Same physical G voicing, expressed in each orientation.
|
||||
assert lo.fingering(3, 2, 0, 0, 0, 3) == lo.chord("G")
|
||||
assert hi.fingering(3, 0, 0, 0, 2, 3) == hi.chord("G")
|
||||
|
||||
|
||||
def test_orientation_cache_no_collision():
|
||||
"""The two orientations must not collide in the fingering cache."""
|
||||
lo = Fretboard.guitar().chord("C")
|
||||
hi = Fretboard.guitar(high_to_low=True).chord("C")
|
||||
assert lo.positions != hi.positions
|
||||
assert lo.positions == tuple(reversed(hi.positions))
|
||||
|
||||
|
||||
def test_to_tab_orientation_agnostic():
|
||||
"""to_tab output is identical regardless of board orientation."""
|
||||
from pytheory import Part
|
||||
notes = ["C4", "E4", "G4"]
|
||||
p_lo = Part("test"); [p_lo.add(n) for n in notes]
|
||||
p_hi = Part("test"); [p_hi.add(n) for n in notes]
|
||||
assert p_lo.to_tab(tuning=Fretboard.guitar()) == \
|
||||
p_hi.to_tab(tuning=Fretboard.guitar(high_to_low=True))
|
||||
|
||||
|
||||
# ── Ergonomic integration tests ─────────────────────────────────────────────
|
||||
|
||||
def test_ergonomic_workflow():
|
||||
|
||||
Reference in New Issue
Block a user