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.
|
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
|
## 0.42.1
|
||||||
|
|
||||||
- **Fretboard tuning support** — `to_tab()` now accepts `Fretboard` objects as
|
- **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 = Fretboard.guitar()
|
||||||
>>> fb.chord("C")
|
>>> 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")
|
>>> 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")
|
>>> 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()``:
|
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
|
Guitars
|
||||||
-------
|
-------
|
||||||
|
|
||||||
`Standard guitar tuning <https://en.wikipedia.org/wiki/Guitar_tunings>`_
|
`Standard guitar tuning <https://en.wikipedia.org/wiki/Guitar_tunings>`_::
|
||||||
(high to low)::
|
|
||||||
|
|
||||||
String 1: E4 (highest)
|
|
||||||
String 2: B3
|
|
||||||
String 3: G3
|
|
||||||
String 4: D3
|
|
||||||
String 5: A2
|
|
||||||
String 6: E2 (lowest)
|
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
|
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).
|
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
|
.. code-block:: pycon
|
||||||
|
|
||||||
>>> from pytheory import Fretboard
|
>>> from pytheory import Fretboard
|
||||||
@@ -192,12 +201,12 @@ on any instrument. It scores each possibility by:
|
|||||||
>>> fb = Fretboard.guitar()
|
>>> fb = Fretboard.guitar()
|
||||||
>>> f = fb.chord("C")
|
>>> f = fb.chord("C")
|
||||||
>>> f
|
>>> 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']
|
>>> f['A']
|
||||||
3
|
3
|
||||||
>>> f[1]
|
>>> f[1]
|
||||||
1
|
3
|
||||||
|
|
||||||
>>> f.identify()
|
>>> f.identify()
|
||||||
'C major'
|
'C major'
|
||||||
@@ -210,11 +219,11 @@ You can also go from fret positions to chord identification:
|
|||||||
|
|
||||||
.. code-block:: pycon
|
.. code-block:: pycon
|
||||||
|
|
||||||
>>> # "What chord am I playing?"
|
>>> # "What chord am I playing?" (positions read low to high)
|
||||||
>>> fb = Fretboard.guitar()
|
>>> fb = Fretboard.guitar()
|
||||||
>>> f = fb.fingering(0, 0, 0, 2, 2, 0)
|
>>> f = fb.fingering(0, 2, 2, 0, 0, 0)
|
||||||
>>> f
|
>>> 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()
|
>>> f.identify()
|
||||||
'E minor'
|
'E minor'
|
||||||
|
|
||||||
@@ -223,14 +232,14 @@ Reading Fingerings
|
|||||||
|
|
||||||
Each position is labeled with its string name. Duplicate string names
|
Each position is labeled with its string name. Duplicate string names
|
||||||
are disambiguated — on a standard guitar, high E appears as ``e`` and
|
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)
|
E|--x-- (muted — low E)
|
||||||
B|--1-- (fret 1 — C)
|
|
||||||
G|--0-- (open — G)
|
|
||||||
D|--2-- (fret 2 — E)
|
|
||||||
A|--3-- (fret 3 — C)
|
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).
|
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"))
|
>>> print(fb.tab("C"))
|
||||||
C major
|
C major
|
||||||
e|--0--
|
|
||||||
B|--1--
|
|
||||||
G|--0--
|
|
||||||
D|--2--
|
|
||||||
A|--3--
|
|
||||||
E|--x--
|
E|--x--
|
||||||
|
A|--3--
|
||||||
|
D|--2--
|
||||||
|
G|--0--
|
||||||
|
B|--1--
|
||||||
|
e|--0--
|
||||||
|
|
||||||
Generating Full Charts
|
Generating Full Charts
|
||||||
----------------------
|
----------------------
|
||||||
@@ -261,7 +270,7 @@ Generate fingerings for every chord at once:
|
|||||||
>>> chart = fb.chart()
|
>>> chart = fb.chart()
|
||||||
|
|
||||||
>>> chart["C"]
|
>>> 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
|
>>> # Works with any instrument
|
||||||
>>> uke_chart = Fretboard.ukulele().chart()
|
>>> uke_chart = Fretboard.ukulele().chart()
|
||||||
@@ -303,19 +312,19 @@ Any instrument can be modeled with custom string tunings:
|
|||||||
|
|
||||||
>>> from pytheory import Tone, Fretboard
|
>>> 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=[
|
>>> bari_uke = Fretboard(tones=[
|
||||||
... Tone.from_string("E4"),
|
|
||||||
... Tone.from_string("B3"),
|
|
||||||
... Tone.from_string("G3"),
|
|
||||||
... Tone.from_string("D3"),
|
... 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=[
|
>>> tres = Fretboard(tones=[
|
||||||
... Tone.from_string("E4"),
|
|
||||||
... Tone.from_string("B3"),
|
|
||||||
... Tone.from_string("G3"),
|
... 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.
|
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 guitar fretboard. Since v0.43.0 fingerings read low to high
|
||||||
standard_tuning = [
|
# (low E first) by default — exactly how tab is conventionally written.
|
||||||
Tone.from_string("E4"), # High E
|
fretboard = Fretboard.guitar()
|
||||||
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)
|
|
||||||
|
|
||||||
# Define flat to sharp note mappings (updated to include all possible flats)
|
# 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#"}
|
flat_to_sharp = {"Ab": "G#", "Bb": "A#", "Db": "C#", "Eb": "D#", "Gb": "F#"}
|
||||||
@@ -26,34 +17,6 @@ print("Standard Guitar Chord Charts:")
|
|||||||
print("-" * 30)
|
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:
|
for chord_name in all_chords:
|
||||||
# Store original chord name for lookup
|
# Store original chord name for lookup
|
||||||
lookup_name = chord_name
|
lookup_name = chord_name
|
||||||
@@ -74,7 +37,7 @@ for chord_name in all_chords:
|
|||||||
try:
|
try:
|
||||||
fingering = chord.fingering(fretboard=fretboard)
|
fingering = chord.fingering(fretboard=fretboard)
|
||||||
print(f"\n{display_name}:")
|
print(f"\n{display_name}:")
|
||||||
print(fingering_to_tab(fingering))
|
print(fingering.tab())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{display_name}: Unable to calculate fingering - {str(e)}")
|
print(f"{display_name}: Unable to calculate fingering - {str(e)}")
|
||||||
# Add more detailed debug information
|
# Add more detailed debug information
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "pytheory"
|
name = "pytheory"
|
||||||
version = "0.42.1"
|
version = "0.43.0"
|
||||||
description = "Music Theory for Humans"
|
description = "Music Theory for Humans"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""PyTheory: Music Theory for Humans."""
|
"""PyTheory: Music Theory for Humans."""
|
||||||
|
|
||||||
__version__ = "0.42.1"
|
__version__ = "0.43.0"
|
||||||
|
|
||||||
from .tones import Tone, Interval
|
from .tones import Tone, Interval
|
||||||
from .systems import System, SYSTEMS, TET
|
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.
|
# Curated override fingerings for common guitar chords in standard tuning.
|
||||||
# Key: chord name, Value: tuple of fret positions (-1 = muted string).
|
# 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 = {
|
GUITAR_OVERRIDES = {
|
||||||
"C": (0, 1, 0, 2, 3, -1),
|
"C": (0, 1, 0, 2, 3, -1),
|
||||||
"D": (2, 3, 2, 0, -1, -1),
|
"D": (2, 3, 2, 0, -1, -1),
|
||||||
@@ -60,11 +61,16 @@ class Fingering:
|
|||||||
3
|
3
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, positions: tuple, string_names: tuple[str, ...], *, fretboard=None) -> None:
|
def __init__(self, positions: tuple, string_names: tuple[str, ...], *,
|
||||||
self.positions = tuple(positions)
|
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
|
self._fretboard = fretboard
|
||||||
# Disambiguate duplicate names: for standard guitar tuning
|
# Disambiguate duplicate names in canonical (high-to-low) order:
|
||||||
# (high-to-low), the first occurrence of a duplicate becomes
|
# the first (higher-pitched) occurrence of a duplicate becomes
|
||||||
# lowercase (e.g. high E → 'e') while the last keeps uppercase.
|
# lowercase (e.g. high E → 'e') while the last keeps uppercase.
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
name_counts = Counter(string_names)
|
name_counts = Counter(string_names)
|
||||||
@@ -77,8 +83,22 @@ class Fingering:
|
|||||||
else:
|
else:
|
||||||
unique_names.append(name)
|
unique_names.append(name)
|
||||||
|
|
||||||
self.string_names = tuple(unique_names)
|
self._string_names = tuple(unique_names)
|
||||||
self._map = dict(zip(self.string_names, self.positions))
|
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:
|
def __repr__(self) -> str:
|
||||||
pairs = ", ".join(
|
pairs = ", ".join(
|
||||||
@@ -114,11 +134,12 @@ class Fingering:
|
|||||||
"""
|
"""
|
||||||
if self._fretboard is None:
|
if self._fretboard is None:
|
||||||
raise ValueError("Cannot resolve tones without a fretboard reference.")
|
raise ValueError("Cannot resolve tones without a fretboard reference.")
|
||||||
tones = []
|
# Zip canonical positions with canonical open tones so they always
|
||||||
for pos, tone in zip(self.positions, self._fretboard.tones):
|
# align regardless of orientation, then present in display order.
|
||||||
if pos is not None:
|
tones = [tone.add(pos)
|
||||||
tones.append(tone.add(pos))
|
for pos, tone in zip(self._positions, self._fretboard._tones)
|
||||||
return tones
|
if pos is not None]
|
||||||
|
return tones if self.high_to_low else list(reversed(tones))
|
||||||
|
|
||||||
def to_chord(self, fretboard=None) -> "Chord":
|
def to_chord(self, fretboard=None) -> "Chord":
|
||||||
"""Apply this fingering to a fretboard, returning a Chord.
|
"""Apply this fingering to a fretboard, returning a Chord.
|
||||||
@@ -131,10 +152,9 @@ class Fingering:
|
|||||||
fb = fretboard or self._fretboard
|
fb = fretboard or self._fretboard
|
||||||
if fb is None:
|
if fb is None:
|
||||||
raise ValueError("No fretboard provided.")
|
raise ValueError("No fretboard provided.")
|
||||||
tones = []
|
tones = [tone.add(pos)
|
||||||
for pos, tone in zip(self.positions, fb.tones):
|
for pos, tone in zip(self._positions, fb._tones)
|
||||||
if pos is not None:
|
if pos is not None]
|
||||||
tones.append(tone.add(pos))
|
|
||||||
return Chord(tones=tones)
|
return Chord(tones=tones)
|
||||||
|
|
||||||
def identify(self) -> Optional[str]:
|
def identify(self) -> Optional[str]:
|
||||||
@@ -151,12 +171,12 @@ class Fingering:
|
|||||||
>>> fb = Fretboard.guitar()
|
>>> fb = Fretboard.guitar()
|
||||||
>>> print(fb.chord("C").tab())
|
>>> print(fb.chord("C").tab())
|
||||||
C
|
C
|
||||||
e|--0--
|
E|--x--
|
||||||
B|--1--
|
|
||||||
G|--0--
|
|
||||||
D|--2--
|
|
||||||
A|--3--
|
A|--3--
|
||||||
E|--0--
|
D|--2--
|
||||||
|
G|--0--
|
||||||
|
B|--1--
|
||||||
|
e|--0--
|
||||||
"""
|
"""
|
||||||
if self._fretboard is None:
|
if self._fretboard is None:
|
||||||
raise ValueError("Cannot render tab without a fretboard reference.")
|
raise ValueError("Cannot render tab without a fretboard reference.")
|
||||||
@@ -306,7 +326,7 @@ class NamedChord:
|
|||||||
return tuple(fingerings)
|
return tuple(fingerings)
|
||||||
|
|
||||||
fingering = []
|
fingering = []
|
||||||
for i, tone in enumerate(fretboard.tones):
|
for i, tone in enumerate(fretboard._tones):
|
||||||
frets = find_fingerings(tone)
|
frets = find_fingerings(tone)
|
||||||
# Always allow muting as an option
|
# Always allow muting as an option
|
||||||
if frets:
|
if frets:
|
||||||
@@ -335,8 +355,13 @@ class NamedChord:
|
|||||||
return tuple(itertools.product(*self._possible_fingerings(fretboard=fretboard)))
|
return tuple(itertools.product(*self._possible_fingerings(fretboard=fretboard)))
|
||||||
|
|
||||||
def _cache_key(self, fretboard):
|
def _cache_key(self, fretboard):
|
||||||
"""Return a hashable key for memoization."""
|
"""Return a hashable key for memoization.
|
||||||
return (self.name, tuple(t.full_name for t in fretboard.tones))
|
|
||||||
|
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):
|
def fingering(self, *, fretboard, multiple=False):
|
||||||
# Check cache first
|
# Check cache first
|
||||||
@@ -348,19 +373,22 @@ class NamedChord:
|
|||||||
if key in _fingering_cache:
|
if key in _fingering_cache:
|
||||||
return _fingering_cache[key]
|
return _fingering_cache[key]
|
||||||
|
|
||||||
# Check for curated guitar chord overrides in standard tuning
|
# Check for curated guitar chord overrides in standard tuning.
|
||||||
tuning = tuple(t.full_name for t in fretboard.tones)
|
# 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:
|
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]
|
override = GUITAR_OVERRIDES[self.name]
|
||||||
if not multiple:
|
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:
|
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||||
_fingering_cache.clear()
|
_fingering_cache.clear()
|
||||||
_fingering_cache[key] = result
|
_fingering_cache[key] = result
|
||||||
return result
|
return result
|
||||||
else:
|
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:
|
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||||
_fingering_multi_cache.clear()
|
_fingering_multi_cache.clear()
|
||||||
_fingering_multi_cache[key] = result
|
_fingering_multi_cache[key] = result
|
||||||
@@ -390,7 +418,7 @@ class NamedChord:
|
|||||||
sounding_names = set()
|
sounding_names = set()
|
||||||
for i, f in enumerate(fingering):
|
for i, f in enumerate(fingering):
|
||||||
if f != -1:
|
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)
|
required = set(t.name for t in self.acceptable_tones)
|
||||||
missing = required - sounding_names
|
missing = required - sounding_names
|
||||||
score -= len(missing) * 5.0
|
score -= len(missing) * 5.0
|
||||||
@@ -433,12 +461,13 @@ class NamedChord:
|
|||||||
if fingers_needed > 4:
|
if fingers_needed > 4:
|
||||||
score -= (fingers_needed - 4) * 5.0
|
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):
|
for i in range(len(fingering) - 1, -1, -1):
|
||||||
f = fingering[i]
|
f = fingering[i]
|
||||||
if f == -1:
|
if f == -1:
|
||||||
continue
|
continue
|
||||||
bass_tone = fretboard.tones[i].add(f)
|
bass_tone = fretboard._tones[i].add(f)
|
||||||
if bass_tone.name == self.tone.name:
|
if bass_tone.name == self.tone.name:
|
||||||
score += 4.0
|
score += 4.0
|
||||||
else:
|
else:
|
||||||
@@ -467,17 +496,20 @@ class NamedChord:
|
|||||||
if s == max_score:
|
if s == max_score:
|
||||||
yield possible_fingering
|
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()])
|
best_fingerings = tuple([g for g in gen()])
|
||||||
if not multiple:
|
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
|
# Bounded cache: clear entirely if over limit
|
||||||
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||||
_fingering_cache.clear()
|
_fingering_cache.clear()
|
||||||
_fingering_cache[key] = result
|
_fingering_cache[key] = result
|
||||||
return result
|
return result
|
||||||
else:
|
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
|
# Bounded cache: clear entirely if over limit
|
||||||
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||||
_fingering_multi_cache.clear()
|
_fingering_multi_cache.clear()
|
||||||
@@ -491,18 +523,21 @@ class NamedChord:
|
|||||||
|
|
||||||
>>> print(CHARTS["western"]["C"].tab(fretboard=Fretboard.guitar()))
|
>>> print(CHARTS["western"]["C"].tab(fretboard=Fretboard.guitar()))
|
||||||
C
|
C
|
||||||
e|--0--
|
E|--x--
|
||||||
B|--1--
|
|
||||||
G|--0--
|
|
||||||
D|--2--
|
|
||||||
A|--3--
|
A|--3--
|
||||||
E|--0--
|
D|--2--
|
||||||
|
G|--0--
|
||||||
|
B|--1--
|
||||||
|
e|--0--
|
||||||
"""
|
"""
|
||||||
fingering = self.fingering(fretboard=fretboard)
|
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]
|
lines = [self.name]
|
||||||
max_name = max(len(n) for n in string_names)
|
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)
|
fret_str = "x" if fret is None else str(fret)
|
||||||
lines.append(f"{name:>{max_name}}|--{fret_str}--")
|
lines.append(f"{name:>{max_name}}|--{fret_str}--")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
+154
-165
@@ -1352,14 +1352,63 @@ class Chord:
|
|||||||
|
|
||||||
|
|
||||||
class Fretboard:
|
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.
|
"""Initialize a Fretboard from a list of open-string Tone objects.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tones: A list of :class:`Tone` instances representing the
|
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:
|
def __repr__(self) -> str:
|
||||||
l = tuple([tone.full_name for tone in self.tones])
|
l = tuple([tone.full_name for tone in self.tones])
|
||||||
@@ -1391,7 +1440,11 @@ class Fretboard:
|
|||||||
Returns:
|
Returns:
|
||||||
A new Fretboard with all strings raised by ``fret`` semitones.
|
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]:
|
def __iter__(self) -> Iterator[Tone]:
|
||||||
"""Iterate over the open-string tones of this fretboard."""
|
"""Iterate over the open-string tones of this fretboard."""
|
||||||
@@ -1399,7 +1452,7 @@ class Fretboard:
|
|||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
"""Return the number of strings on this fretboard."""
|
"""Return the number of strings on this fretboard."""
|
||||||
return len(self.tones)
|
return len(self._tones)
|
||||||
|
|
||||||
INSTRUMENTS = [
|
INSTRUMENTS = [
|
||||||
"guitar", "twelve_string", "bass", "ukulele",
|
"guitar", "twelve_string", "bass", "ukulele",
|
||||||
@@ -1423,84 +1476,76 @@ class Fretboard:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@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.
|
"""Guitar with the given tuning and optional capo.
|
||||||
|
|
||||||
Args:
|
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,
|
Built-in tunings: standard, drop d, open g, open d,
|
||||||
open e, open a, dadgad, half step down.
|
open e, open a, dadgad, half step down.
|
||||||
capo: Fret number for the capo (0 = no capo). Raises all
|
capo: Fret number for the capo (0 = no capo). Raises all
|
||||||
strings by this many semitones.
|
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
|
from .tones import Tone
|
||||||
if isinstance(tuning, str):
|
if isinstance(tuning, str):
|
||||||
tuning = cls.TUNINGS[tuning]
|
# Built-in tunings are defined canonically (high to low).
|
||||||
fb = cls(tones=[Tone.from_string(t, system="western") for t in tuning])
|
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:
|
if capo:
|
||||||
fb = fb.capo(capo)
|
fb = fb.capo(capo)
|
||||||
return fb
|
return fb
|
||||||
|
|
||||||
@classmethod
|
@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.
|
"""Standard bass guitar tuning.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
five_string: If True, adds a low B string (B0).
|
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"]
|
strings = ["G2", "D2", "A1", "E1"]
|
||||||
if five_string:
|
if five_string:
|
||||||
strings.append("B0")
|
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
|
@classmethod
|
||||||
def ukulele(cls) -> Fretboard:
|
def ukulele(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard ukulele tuning (A4 E4 C4 G4).
|
"""Standard ukulele tuning (A4 E4 C4 G4).
|
||||||
|
|
||||||
Re-entrant tuning: the G4 string is higher than C4.
|
Re-entrant tuning: the G4 string is higher than C4.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["A4", "E4", "C4", "G4"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mandolin(cls) -> Fretboard:
|
def mandolin(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard mandolin tuning (E5 A4 D4 G3).
|
"""Standard mandolin tuning (E5 A4 D4 G3).
|
||||||
|
|
||||||
Tuned in fifths, same as a violin but one octave relationship.
|
Tuned in fifths, same as a violin but one octave relationship.
|
||||||
Strings are typically doubled (paired courses).
|
Strings are typically doubled (paired courses).
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["E5", "A4", "D4", "G3"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mandola(cls) -> Fretboard:
|
def mandola(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard mandola tuning (A4 D4 G3 C3).
|
"""Standard mandola tuning (A4 D4 G3 C3).
|
||||||
|
|
||||||
The mandola (or tenor mandola) is to the mandolin what the
|
The mandola (or tenor mandola) is to the mandolin what the
|
||||||
viola is to the violin — a fifth lower, with a warmer,
|
viola is to the violin — a fifth lower, with a warmer,
|
||||||
darker tone. Tuned in fifths like all the mandolin family.
|
darker tone. Tuned in fifths like all the mandolin family.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["A4", "D4", "G3", "C3"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def octave_mandolin(cls) -> Fretboard:
|
def octave_mandolin(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Octave mandolin tuning (E4 A3 D3 G2).
|
"""Octave mandolin tuning (E4 A3 D3 G2).
|
||||||
|
|
||||||
Also called the octave mandola in European terminology.
|
Also called the octave mandola in European terminology.
|
||||||
@@ -1508,84 +1553,57 @@ class Fretboard:
|
|||||||
family's cello-to-violin relationship. Popular in Irish
|
family's cello-to-violin relationship. Popular in Irish
|
||||||
and Celtic folk music.
|
and Celtic folk music.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["E4", "A3", "D3", "G2"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mandocello(cls) -> Fretboard:
|
def mandocello(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Mandocello tuning (A3 D3 G2 C2).
|
"""Mandocello tuning (A3 D3 G2 C2).
|
||||||
|
|
||||||
The bass of the mandolin family. Tuned like a cello — an
|
The bass of the mandolin family. Tuned like a cello — an
|
||||||
octave below the mandola. Rare but beautiful; used in
|
octave below the mandola. Rare but beautiful; used in
|
||||||
mandolin orchestras.
|
mandolin orchestras.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["A3", "D3", "G2", "C2"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def violin(cls) -> Fretboard:
|
def violin(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard violin tuning (E5 A4 D4 G3).
|
"""Standard violin tuning (E5 A4 D4 G3).
|
||||||
|
|
||||||
Tuned in perfect fifths. The violin has no frets — intonation
|
Tuned in perfect fifths. The violin has no frets — intonation
|
||||||
is continuous, allowing vibrato and microtonal inflections
|
is continuous, allowing vibrato and microtonal inflections
|
||||||
not possible on fretted instruments.
|
not possible on fretted instruments.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["E5", "A4", "D4", "G3"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def viola(cls) -> Fretboard:
|
def viola(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard viola tuning (A4 D4 G3 C3).
|
"""Standard viola tuning (A4 D4 G3 C3).
|
||||||
|
|
||||||
A perfect fifth below the violin. The viola's darker, warmer
|
A perfect fifth below the violin. The viola's darker, warmer
|
||||||
tone comes from its larger body and lower register.
|
tone comes from its larger body and lower register.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["A4", "D4", "G3", "C3"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cello(cls) -> Fretboard:
|
def cello(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard cello tuning (A3 D3 G2 C2).
|
"""Standard cello tuning (A3 D3 G2 C2).
|
||||||
|
|
||||||
An octave below the viola. Tuned in fifths. The cello spans
|
An octave below the viola. Tuned in fifths. The cello spans
|
||||||
the range of the human voice — tenor through bass.
|
the range of the human voice — tenor through bass.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["A3", "D3", "G2", "C2"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@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.
|
"""Banjo with the given tuning.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tuning: ``"open g"`` (default, bluegrass) or ``"open d"``
|
tuning: ``"open g"`` (default, bluegrass) or ``"open d"``
|
||||||
(old-time, clawhammer). The 5th string is a high
|
(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
|
Standard open G: G4 D3 G3 B3 D4 (5th string is the short
|
||||||
high G4 drone).
|
high G4 drone).
|
||||||
@@ -1597,11 +1615,12 @@ class Fretboard:
|
|||||||
"double c": ("D4", "C4", "G3", "C3", "G4"),
|
"double c": ("D4", "C4", "G3", "C3", "G4"),
|
||||||
}
|
}
|
||||||
if isinstance(tuning, str):
|
if isinstance(tuning, str):
|
||||||
tuning = tunings[tuning]
|
return cls._from_canonical(tunings[tuning], high_to_low)
|
||||||
return cls(tones=[Tone.from_string(t, system="western") for t in tuning])
|
return cls(tones=[Tone.from_string(t, system="western") for t in tuning],
|
||||||
|
high_to_low=high_to_low)
|
||||||
|
|
||||||
@classmethod
|
@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).
|
"""Standard double bass (upright bass) tuning (G2 D2 A1 E1).
|
||||||
|
|
||||||
The largest and lowest-pitched bowed string instrument in the
|
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.
|
The 5-string double bass adds a low B0 or C1.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["G2", "D2", "A1", "E1"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def harp(cls) -> Fretboard:
|
def harp(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Concert harp strings — 47 strings spanning C1 to G7.
|
"""Concert harp strings — 47 strings spanning C1 to G7.
|
||||||
|
|
||||||
The pedal harp has 7 strings per octave (one per note name),
|
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
|
This returns the full set of 47 strings in the default
|
||||||
Cb (enharmonic B) tuning.
|
Cb (enharmonic B) tuning.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
|
||||||
# 47 strings: C1 to G7, one per diatonic note
|
# 47 strings: C1 to G7, one per diatonic note
|
||||||
notes = ["C", "D", "E", "F", "G", "A", "B"]
|
notes = ["C", "D", "E", "F", "G", "A", "B"]
|
||||||
strings = []
|
strings = []
|
||||||
@@ -1643,30 +1655,33 @@ class Fretboard:
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
# Harp strings are high to low
|
# Canonical (high to low)
|
||||||
strings.reverse()
|
strings.reverse()
|
||||||
return cls(tones=[Tone.from_string(s, system="western") for s in strings])
|
return cls._from_canonical(strings, high_to_low)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pedal_steel(cls) -> Fretboard:
|
def pedal_steel(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Pedal steel guitar — E9 Nashville tuning (10 strings).
|
"""Pedal steel guitar — E9 Nashville tuning (10 strings).
|
||||||
|
|
||||||
The standard tuning for country music. The pedal steel has
|
The standard tuning for country music. The pedal steel has
|
||||||
foot pedals and knee levers that change string pitches during
|
foot pedals and knee levers that change string pitches during
|
||||||
play, enabling its signature swooping, crying sound.
|
play, enabling its signature swooping, crying sound.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
# E9 Nashville tuning (canonical: high to low)
|
||||||
# E9 Nashville tuning (high to low)
|
|
||||||
strings = ["F#4", "D#4", "G#3", "E3", "B3", "G#3",
|
strings = ["F#4", "D#4", "G#3", "E3", "B3", "G#3",
|
||||||
"F#3", "E3", "D3", "B2"]
|
"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
|
@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.
|
"""Bouzouki tuning.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
variant: ``"irish"`` (default, GDAD) or ``"greek"`` (CFAD).
|
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
|
The Irish bouzouki is a staple of Celtic music, usually tuned
|
||||||
in unison or octave pairs. The Greek bouzouki traditionally
|
in unison or octave pairs. The Greek bouzouki traditionally
|
||||||
@@ -1678,11 +1693,12 @@ class Fretboard:
|
|||||||
"greek": ("D4", "A3", "F3", "C3"),
|
"greek": ("D4", "A3", "F3", "C3"),
|
||||||
}
|
}
|
||||||
if isinstance(variant, str):
|
if isinstance(variant, str):
|
||||||
variant = tunings[variant]
|
return cls._from_canonical(tunings[variant], high_to_low)
|
||||||
return cls(tones=[Tone.from_string(t, system="western") for t in variant])
|
return cls(tones=[Tone.from_string(t, system="western") for t in variant],
|
||||||
|
high_to_low=high_to_low)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def oud(cls) -> Fretboard:
|
def oud(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard Arabic oud tuning (C4 G3 D3 A2 G2 C2).
|
"""Standard Arabic oud tuning (C4 G3 D3 A2 G2 C2).
|
||||||
|
|
||||||
The oud is the ancestor of the European lute and the defining
|
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),
|
essential to maqam performance. 6 courses (11 strings),
|
||||||
typically tuned in fourths.
|
typically tuned in fourths.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
|
||||||
strings = ["C4", "G3", "D3", "A2", "G2", "C2"]
|
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
|
@classmethod
|
||||||
def sitar(cls) -> Fretboard:
|
def sitar(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Sitar main playing strings (approximation).
|
"""Sitar main playing strings (approximation).
|
||||||
|
|
||||||
The sitar typically has 6-7 main strings and 11-13 sympathetic
|
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).
|
Main strings: Sa Sa Pa Sa Re Sa Ma (approximated in 12-TET).
|
||||||
Represented here as the most common Ravi Shankar school tuning.
|
Represented here as the most common Ravi Shankar school tuning.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
|
||||||
# Common Ravi Shankar tuning mapped to Western notes
|
# Common Ravi Shankar tuning mapped to Western notes
|
||||||
# (sitar is tuned relative to Sa, typically C# or D)
|
# (sitar is tuned relative to Sa, typically C# or D)
|
||||||
strings = ["C4", "C3", "G3", "C3", "D3", "C2", "F2"]
|
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
|
@classmethod
|
||||||
def shamisen(cls) -> Fretboard:
|
def shamisen(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard shamisen tuning — honchoshi (C4 G3 C3).
|
"""Standard shamisen tuning — honchoshi (C4 G3 C3).
|
||||||
|
|
||||||
The shamisen is a 3-stringed Japanese instrument played with
|
The shamisen is a 3-stringed Japanese instrument played with
|
||||||
@@ -1723,15 +1737,10 @@ class Fretboard:
|
|||||||
- niagari (二上り): root-5th-2nd (raises 2nd string)
|
- niagari (二上り): root-5th-2nd (raises 2nd string)
|
||||||
- sansagari (三下り): root-5th-b7th (lowers 3rd string)
|
- sansagari (三下り): root-5th-b7th (lowers 3rd string)
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["C4", "G3", "C3"], high_to_low)
|
||||||
return cls(tones=[
|
|
||||||
Tone.from_string("C4", system="western"),
|
|
||||||
Tone.from_string("G3", system="western"),
|
|
||||||
Tone.from_string("C3", system="western"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def erhu(cls) -> Fretboard:
|
def erhu(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard erhu tuning (A4 D4).
|
"""Standard erhu tuning (A4 D4).
|
||||||
|
|
||||||
The erhu is a 2-stringed Chinese bowed instrument with a
|
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,
|
— the player presses the strings without touching the neck,
|
||||||
allowing continuous pitch bending.
|
allowing continuous pitch bending.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["A4", "D4"], high_to_low)
|
||||||
return cls(tones=[
|
|
||||||
Tone.from_string("A4", system="western"),
|
|
||||||
Tone.from_string("D4", system="western"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def charango(cls) -> Fretboard:
|
def charango(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard charango tuning (E5 A4 E5 C5 G4).
|
"""Standard charango tuning (E5 A4 E5 C5 G4).
|
||||||
|
|
||||||
A small Andean stringed instrument, traditionally made from
|
A small Andean stringed instrument, traditionally made from
|
||||||
@@ -1754,54 +1759,38 @@ class Fretboard:
|
|||||||
— the 3rd course (E5) is the highest pitched, creating the
|
— the 3rd course (E5) is the highest pitched, creating the
|
||||||
charango's bright, sparkling sound.
|
charango's bright, sparkling sound.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["E5", "A4", "E5", "C5", "G4"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pipa(cls) -> Fretboard:
|
def pipa(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard pipa tuning (D4 A3 E3 A2).
|
"""Standard pipa tuning (D4 A3 E3 A2).
|
||||||
|
|
||||||
The pipa is a 4-stringed Chinese lute with a pear-shaped
|
The pipa is a 4-stringed Chinese lute with a pear-shaped
|
||||||
body, dating back over 2000 years. Known for its percussive
|
body, dating back over 2000 years. Known for its percussive
|
||||||
attack and rapid tremolo technique.
|
attack and rapid tremolo technique.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["D4", "A3", "E3", "A2"], high_to_low)
|
||||||
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"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def balalaika(cls) -> Fretboard:
|
def balalaika(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Standard balalaika prima tuning (A4 E4 E4).
|
"""Standard balalaika prima tuning (A4 E4 E4).
|
||||||
|
|
||||||
The Russian balalaika has a distinctive triangular body and
|
The Russian balalaika has a distinctive triangular body and
|
||||||
3 strings. The two lower strings are tuned in unison — a
|
3 strings. The two lower strings are tuned in unison — a
|
||||||
unique feature that gives it a natural chorus effect.
|
unique feature that gives it a natural chorus effect.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
return cls._from_canonical(["A4", "E4", "E4"], high_to_low)
|
||||||
return cls(tones=[
|
|
||||||
Tone.from_string("A4", system="western"),
|
|
||||||
Tone.from_string("E4", system="western"),
|
|
||||||
Tone.from_string("E4", system="western"),
|
|
||||||
])
|
|
||||||
|
|
||||||
@classmethod
|
@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.
|
"""Piano or keyboard with the given number of keys.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
keys: Number of keys (default 88 for a full piano).
|
keys: Number of keys (default 88 for a full piano).
|
||||||
Common sizes: 25, 37, 49, 61, 76, 88.
|
Common sizes: 25, 37, 49, 61, 76, 88.
|
||||||
start: The lowest note (default ``"A0"`` for standard piano).
|
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) —
|
A full 88-key piano spans A0 (27.5 Hz) to C8 (4186 Hz) —
|
||||||
the widest range of any standard acoustic instrument.
|
the widest range of any standard acoustic instrument.
|
||||||
@@ -1815,13 +1804,12 @@ class Fretboard:
|
|||||||
"""
|
"""
|
||||||
from .tones import Tone
|
from .tones import Tone
|
||||||
start_tone = Tone.from_string(start, system="western")
|
start_tone = Tone.from_string(start, system="western")
|
||||||
tones = []
|
# Built high-to-low (canonical): highest key first, down to `start`.
|
||||||
for i in range(keys - 1, -1, -1):
|
tones = [start_tone.add(i) for i in range(keys - 1, -1, -1)]
|
||||||
tones.append(start_tone.add(i))
|
return cls(tones=tones, high_to_low=high_to_low, _canonical=True)
|
||||||
return cls(tones=tones)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def lute(cls) -> Fretboard:
|
def lute(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""Renaissance lute in G tuning (6 courses).
|
"""Renaissance lute in G tuning (6 courses).
|
||||||
|
|
||||||
The European lute was the dominant instrument of the
|
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
|
a major third between the 3rd and 4th courses — the
|
||||||
same intervallic pattern as a modern guitar.
|
same intervallic pattern as a modern guitar.
|
||||||
"""
|
"""
|
||||||
from .tones import Tone
|
|
||||||
strings = ["G4", "D4", "A3", "F3", "C3", "G2"]
|
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
|
@classmethod
|
||||||
def twelve_string(cls) -> Fretboard:
|
def twelve_string(cls, high_to_low: bool = False) -> Fretboard:
|
||||||
"""12-string guitar in standard tuning.
|
"""12-string guitar in standard tuning.
|
||||||
|
|
||||||
The lower 4 courses are doubled at the octave; the upper 2
|
The lower 4 courses are doubled at the octave; the upper 2
|
||||||
are doubled in unison. This creates the characteristic
|
are doubled in unison. This creates the characteristic
|
||||||
shimmering, chorus-like sound.
|
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 = [
|
strings = [
|
||||||
"E4", "E4", # 1st course (unison)
|
"E4", "E4", # 1st course (unison)
|
||||||
"B3", "B3", # 2nd course (unison)
|
"B3", "B3", # 2nd course (unison)
|
||||||
@@ -1852,7 +1838,7 @@ class Fretboard:
|
|||||||
"A3", "A2", # 5th course (octave)
|
"A3", "A2", # 5th course (octave)
|
||||||
"E3", "E2", # 6th 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:
|
def scale_diagram(self, scale, frets: int = 12, chord=None) -> str:
|
||||||
"""Render an ASCII diagram showing where scale notes fall on the neck.
|
"""Render an ASCII diagram showing where scale notes fall on the neck.
|
||||||
@@ -1927,7 +1913,7 @@ class Fretboard:
|
|||||||
|
|
||||||
>>> fb = Fretboard.guitar()
|
>>> fb = Fretboard.guitar()
|
||||||
>>> fb.chord("G")
|
>>> 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
|
from .charts import CHARTS
|
||||||
return CHARTS[system][name].fingering(fretboard=self)
|
return CHARTS[system][name].fingering(fretboard=self)
|
||||||
@@ -1945,7 +1931,7 @@ class Fretboard:
|
|||||||
|
|
||||||
>>> fb = Fretboard.guitar()
|
>>> fb = Fretboard.guitar()
|
||||||
>>> fb["G"]
|
>>> 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)
|
return self.chord(name)
|
||||||
|
|
||||||
@@ -1964,12 +1950,12 @@ class Fretboard:
|
|||||||
>>> fb = Fretboard.guitar()
|
>>> fb = Fretboard.guitar()
|
||||||
>>> print(fb.tab("Am"))
|
>>> print(fb.tab("Am"))
|
||||||
A minor
|
A minor
|
||||||
e|--0--
|
E|--x--
|
||||||
B|--1--
|
|
||||||
G|--2--
|
|
||||||
D|--2--
|
|
||||||
A|--0--
|
A|--0--
|
||||||
E|--0--
|
D|--2--
|
||||||
|
G|--2--
|
||||||
|
B|--1--
|
||||||
|
e|--0--
|
||||||
"""
|
"""
|
||||||
return self.chord(name, system=system).tab()
|
return self.chord(name, system=system).tab()
|
||||||
|
|
||||||
@@ -1984,7 +1970,7 @@ class Fretboard:
|
|||||||
>>> fb = Fretboard.guitar()
|
>>> fb = Fretboard.guitar()
|
||||||
>>> chart = fb.chart()
|
>>> chart = fb.chart()
|
||||||
>>> chart["Am7"]
|
>>> 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
|
from .charts import charts_for_fretboard, CHARTS
|
||||||
return charts_for_fretboard(chart=CHARTS[system], fretboard=self)
|
return charts_for_fretboard(chart=CHARTS[system], fretboard=self)
|
||||||
@@ -2009,13 +1995,16 @@ class Fretboard:
|
|||||||
"""
|
"""
|
||||||
from .charts import Fingering
|
from .charts import Fingering
|
||||||
|
|
||||||
if not len(positions) == len(self.tones):
|
if not len(positions) == len(self._tones):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"The number of positions must match the number of tones (strings)."
|
"The number of positions must match the number of tones (strings)."
|
||||||
)
|
)
|
||||||
|
|
||||||
string_names = tuple(t.name for t in self.tones)
|
# Positions arrive in this board's orientation; canonicalise them
|
||||||
return Fingering(positions, string_names, fretboard=self)
|
# (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]:
|
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)
|
fingering = self._fretboard.chord(chord_name)
|
||||||
|
|
||||||
# Get the sounding tones (skips muted strings)
|
# Get the sounding tones (skips muted strings)
|
||||||
tones = fingering.tones # list of Tone objects, high to low
|
tones = fingering.tones
|
||||||
|
|
||||||
if not tones:
|
if not tones:
|
||||||
self.rest(duration)
|
self.rest(duration)
|
||||||
return self
|
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":
|
if direction == "down":
|
||||||
strum_tones = list(reversed(tones))
|
strum_tones = low_to_high
|
||||||
else:
|
else:
|
||||||
strum_tones = list(tones)
|
strum_tones = list(reversed(low_to_high))
|
||||||
|
|
||||||
if hasattr(duration, 'value'):
|
if hasattr(duration, 'value'):
|
||||||
total_beats = duration.value
|
total_beats = duration.value
|
||||||
@@ -3826,8 +3828,9 @@ class Part:
|
|||||||
open_midis = list(self._TAB_TUNINGS[tuning])
|
open_midis = list(self._TAB_TUNINGS[tuning])
|
||||||
labels = list(self._TAB_LABELS[tuning])
|
labels = list(self._TAB_LABELS[tuning])
|
||||||
elif hasattr(tuning, "tones"):
|
elif hasattr(tuning, "tones"):
|
||||||
# Fretboard object — tones are high-to-low, reverse for low-to-high
|
# Fretboard object — sort by pitch so we get low-to-high
|
||||||
fb_tones = list(reversed(tuning.tones))
|
# 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]
|
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]
|
labels = [t.name if len(t.name) <= 2 else t.name[0] for t in fb_tones]
|
||||||
else:
|
else:
|
||||||
|
|||||||
+100
-33
@@ -482,7 +482,8 @@ def test_fretboard_creation():
|
|||||||
Tone(name="A", octave=2),
|
Tone(name="A", octave=2),
|
||||||
Tone(name="E", 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 len(fretboard.tones) == 6
|
||||||
assert fretboard.tones[0].full_name == "E4"
|
assert fretboard.tones[0].full_name == "E4"
|
||||||
assert fretboard.tones[-1].full_name == "E2"
|
assert fretboard.tones[-1].full_name == "E2"
|
||||||
@@ -505,7 +506,8 @@ def guitar_fretboard():
|
|||||||
Tone.from_string("A2"),
|
Tone.from_string("A2"),
|
||||||
Tone.from_string("E2"),
|
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):
|
def test_chord_fingering_c(guitar_fretboard):
|
||||||
@@ -1767,28 +1769,32 @@ def test_chord_contains_tone():
|
|||||||
def test_fretboard_guitar():
|
def test_fretboard_guitar():
|
||||||
fb = Fretboard.guitar()
|
fb = Fretboard.guitar()
|
||||||
assert len(fb) == 6
|
assert len(fb) == 6
|
||||||
|
# Low-to-high by default (v0.43.0).
|
||||||
names = [t.name for t in fb]
|
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():
|
def test_fretboard_guitar_octaves():
|
||||||
fb = Fretboard.guitar()
|
fb = Fretboard.guitar()
|
||||||
octaves = [t.octave for t in fb]
|
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():
|
def test_fretboard_bass():
|
||||||
fb = Fretboard.bass()
|
fb = Fretboard.bass()
|
||||||
assert len(fb) == 4
|
assert len(fb) == 4
|
||||||
names = [t.name for t in fb]
|
names = [t.name for t in fb]
|
||||||
assert names == ["G", "D", "A", "E"]
|
assert names == ["E", "A", "D", "G"]
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_ukulele():
|
def test_fretboard_ukulele():
|
||||||
fb = Fretboard.ukulele()
|
fb = Fretboard.ukulele()
|
||||||
assert len(fb) == 4
|
assert len(fb) == 4
|
||||||
names = [t.name for t in fb]
|
names = [t.name for t in fb]
|
||||||
assert names == ["A", "E", "C", "G"]
|
assert names == ["G", "C", "E", "A"]
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_iter():
|
def test_fretboard_iter():
|
||||||
@@ -1821,8 +1827,9 @@ def test_fretboard_ukulele_fingerings():
|
|||||||
def test_fretboard_guitar_drop_d():
|
def test_fretboard_guitar_drop_d():
|
||||||
fb = Fretboard.guitar("drop d")
|
fb = Fretboard.guitar("drop d")
|
||||||
assert len(fb) == 6
|
assert len(fb) == 6
|
||||||
assert fb.tones[-1].name == "D"
|
# Low-to-high: the dropped low D is now the first string.
|
||||||
assert fb.tones[-1].octave == 2
|
assert fb.tones[0].name == "D"
|
||||||
|
assert fb.tones[0].octave == 2
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_guitar_open_g():
|
def test_fretboard_guitar_open_g():
|
||||||
@@ -1840,7 +1847,8 @@ def test_fretboard_guitar_custom_tuple():
|
|||||||
def test_fretboard_bass_five_string():
|
def test_fretboard_bass_five_string():
|
||||||
fb = Fretboard.bass(five_string=True)
|
fb = Fretboard.bass(five_string=True)
|
||||||
assert len(fb) == 5
|
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():
|
def test_fretboard_tunings_dict():
|
||||||
@@ -1852,36 +1860,37 @@ def test_fretboard_tunings_dict():
|
|||||||
def test_fretboard_mandolin():
|
def test_fretboard_mandolin():
|
||||||
fb = Fretboard.mandolin()
|
fb = Fretboard.mandolin()
|
||||||
assert len(fb) == 4
|
assert len(fb) == 4
|
||||||
assert fb.tones[0].name == "E"
|
# Low-to-high.
|
||||||
assert fb.tones[-1].name == "G"
|
assert fb.tones[0].name == "G"
|
||||||
|
assert fb.tones[-1].name == "E"
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_violin():
|
def test_fretboard_violin():
|
||||||
fb = Fretboard.violin()
|
fb = Fretboard.violin()
|
||||||
assert len(fb) == 4
|
assert len(fb) == 4
|
||||||
names = [t.name for t in fb]
|
names = [t.name for t in fb]
|
||||||
assert names == ["E", "A", "D", "G"]
|
assert names == ["G", "D", "A", "E"]
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_viola():
|
def test_fretboard_viola():
|
||||||
fb = Fretboard.viola()
|
fb = Fretboard.viola()
|
||||||
assert len(fb) == 4
|
assert len(fb) == 4
|
||||||
names = [t.name for t in fb]
|
names = [t.name for t in fb]
|
||||||
assert names == ["A", "D", "G", "C"]
|
assert names == ["C", "G", "D", "A"]
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_cello():
|
def test_fretboard_cello():
|
||||||
fb = Fretboard.cello()
|
fb = Fretboard.cello()
|
||||||
assert len(fb) == 4
|
assert len(fb) == 4
|
||||||
names = [t.name for t in fb]
|
names = [t.name for t in fb]
|
||||||
assert names == ["A", "D", "G", "C"]
|
assert names == ["C", "G", "D", "A"]
|
||||||
assert fb.tones[0].octave == 3
|
assert fb.tones[0].octave == 2
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_banjo():
|
def test_fretboard_banjo():
|
||||||
fb = Fretboard.banjo()
|
fb = Fretboard.banjo()
|
||||||
assert len(fb) == 5
|
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():
|
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."""
|
"""Violin strings should be a perfect 5th apart."""
|
||||||
fb = Fretboard.violin()
|
fb = Fretboard.violin()
|
||||||
for i in range(len(fb.tones) - 1):
|
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"
|
assert interval == 7, f"Strings {i} and {i+1} not a 5th apart"
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_octave_mandolin():
|
def test_fretboard_octave_mandolin():
|
||||||
fb = Fretboard.octave_mandolin()
|
fb = Fretboard.octave_mandolin()
|
||||||
assert len(fb) == 4
|
assert len(fb) == 4
|
||||||
assert fb.tones[0].name == "E"
|
assert fb.tones[-1].name == "E"
|
||||||
assert fb.tones[0].octave == 4
|
assert fb.tones[-1].octave == 4
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_mandocello():
|
def test_fretboard_mandocello():
|
||||||
fb = Fretboard.mandocello()
|
fb = Fretboard.mandocello()
|
||||||
assert len(fb) == 4
|
assert len(fb) == 4
|
||||||
names = [t.name for t in fb]
|
names = [t.name for t in fb]
|
||||||
assert names == ["A", "D", "G", "C"]
|
assert names == ["C", "G", "D", "A"]
|
||||||
assert fb.tones[0].octave == 3
|
assert fb.tones[0].octave == 2
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_double_bass():
|
def test_fretboard_double_bass():
|
||||||
fb = Fretboard.double_bass()
|
fb = Fretboard.double_bass()
|
||||||
assert len(fb) == 4
|
assert len(fb) == 4
|
||||||
names = [t.name for t in fb]
|
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():
|
def test_fretboard_double_bass_tuned_in_fourths():
|
||||||
fb = Fretboard.double_bass()
|
fb = Fretboard.double_bass()
|
||||||
for i in range(len(fb.tones) - 1):
|
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"
|
assert interval == 5, f"Strings {i} and {i+1} not a 4th apart"
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_harp():
|
def test_fretboard_harp():
|
||||||
fb = Fretboard.harp()
|
fb = Fretboard.harp()
|
||||||
assert len(fb) == 47
|
assert len(fb) == 47
|
||||||
assert fb.tones[0].name == "G"
|
# Low-to-high.
|
||||||
assert fb.tones[0].octave == 7
|
assert fb.tones[0].name == "C"
|
||||||
assert fb.tones[-1].name == "C"
|
assert fb.tones[0].octave == 1
|
||||||
assert fb.tones[-1].octave == 1
|
assert fb.tones[-1].name == "G"
|
||||||
|
assert fb.tones[-1].octave == 7
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_pedal_steel():
|
def test_fretboard_pedal_steel():
|
||||||
@@ -1950,7 +1962,7 @@ def test_mandolin_family_fifths():
|
|||||||
for name in ["mandolin", "mandola", "octave_mandolin", "mandocello"]:
|
for name in ["mandolin", "mandola", "octave_mandolin", "mandocello"]:
|
||||||
fb = getattr(Fretboard, name)()
|
fb = getattr(Fretboard, name)()
|
||||||
for i in range(len(fb.tones) - 1):
|
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"
|
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():
|
def test_fretboard_erhu():
|
||||||
fb = Fretboard.erhu()
|
fb = Fretboard.erhu()
|
||||||
assert len(fb) == 2
|
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():
|
def test_fretboard_bouzouki_irish():
|
||||||
@@ -2003,8 +2015,8 @@ def test_fretboard_charango():
|
|||||||
def test_fretboard_balalaika():
|
def test_fretboard_balalaika():
|
||||||
fb = Fretboard.balalaika()
|
fb = Fretboard.balalaika()
|
||||||
assert len(fb) == 3
|
assert len(fb) == 3
|
||||||
# Two unison strings
|
# Two unison strings (now the lowest two, low-to-high)
|
||||||
assert fb.tones[1].name == fb.tones[2].name
|
assert fb.tones[0].name == fb.tones[1].name
|
||||||
|
|
||||||
|
|
||||||
def test_fretboard_lute():
|
def test_fretboard_lute():
|
||||||
@@ -2030,8 +2042,9 @@ def test_keyboard_88():
|
|||||||
def test_keyboard_25():
|
def test_keyboard_25():
|
||||||
kb = Fretboard.keyboard(25, "C3")
|
kb = Fretboard.keyboard(25, "C3")
|
||||||
assert len(kb) == 25
|
assert len(kb) == 25
|
||||||
assert kb.tones[-1].name == "C"
|
# Low-to-high: the start note is now the first key.
|
||||||
assert kb.tones[-1].octave == 3
|
assert kb.tones[0].name == "C"
|
||||||
|
assert kb.tones[0].octave == 3
|
||||||
|
|
||||||
|
|
||||||
def test_keyboard_custom():
|
def test_keyboard_custom():
|
||||||
@@ -2039,6 +2052,60 @@ def test_keyboard_custom():
|
|||||||
assert len(kb) == 61
|
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 ─────────────────────────────────────────────
|
# ── Ergonomic integration tests ─────────────────────────────────────────────
|
||||||
|
|
||||||
def test_ergonomic_workflow():
|
def test_ergonomic_workflow():
|
||||||
|
|||||||
Reference in New Issue
Block a user