From fcfba20ec50e8b0e210f9b30bb6cb066f7fc53d1 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 22 Mar 2026 06:48:25 -0400 Subject: [PATCH] Add mandolin, mandola, violin, viola, cello, banjo, 12-string presets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Fretboard presets: - Fretboard.mandolin() — E5 A4 D4 G3 (tuned in 5ths) - Fretboard.mandola() — A4 D4 G3 C3 (octave below mandolin) - Fretboard.violin() — E5 A4 D4 G3 (same as mandolin) - Fretboard.viola() — A4 D4 G3 C3 (5th below violin) - Fretboard.cello() — A3 D3 G2 C2 (octave below viola) - Fretboard.banjo(tuning) — open G, open D, double C - Fretboard.twelve_string() — 12-string guitar (6 doubled courses) Updated fretboard docs with string family section and custom instrument examples (mandola, baritone ukulele, upright bass). 347 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/guide/fretboard.rst | 70 ++++++++++++++++++---- pytheory/chords.py | 124 ++++++++++++++++++++++++++++++++++++++- test_pytheory.py | 53 +++++++++++++++++ 3 files changed, 233 insertions(+), 14 deletions(-) diff --git a/docs/guide/fretboard.rst b/docs/guide/fretboard.rst index 49e37bb..0ad40be 100644 --- a/docs/guide/fretboard.rst +++ b/docs/guide/fretboard.rst @@ -31,10 +31,21 @@ Preset Tunings from pytheory import Fretboard + # Guitars guitar = Fretboard.guitar() # Standard EADGBE + twelve = Fretboard.twelve_string() # 12-string (6 doubled courses) bass = Fretboard.bass() # Standard EADG bass5 = Fretboard.bass(five_string=True) # 5-string BEADG - ukulele = Fretboard.ukulele() # GCEA (re-entrant) + + # Plucked strings + ukulele = Fretboard.ukulele() # GCEA (re-entrant) + mandolin = Fretboard.mandolin() # GDAE (tuned in 5ths) + banjo = Fretboard.banjo() # Open G (5-string with drone) + + # Bowed strings + violin = Fretboard.violin() # GDAE + viola = Fretboard.viola() # CGDA (5th below violin) + cello = Fretboard.cello() # CGDA (octave below viola) Alternate Guitar Tunings ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -58,31 +69,64 @@ PyTheory supports several common alternate tunings, including # Custom tuning with any notes custom = Fretboard.guitar(("D4", "A3", "F#3", "D3", "A2", "D2")) +The String Family +----------------- + +All four members of the orchestral string family are tuned in +`perfect fifths `_ +(7 semitones between adjacent strings): + +.. code-block:: python + + violin = Fretboard.violin() # E5 A4 D4 G3 — soprano + viola = Fretboard.viola() # A4 D4 G3 C3 — alto (5th below violin) + cello = Fretboard.cello() # A3 D3 G2 C2 — tenor/bass (octave below viola) + +Unlike fretted instruments, bowed strings have no frets — the player +can produce any pitch along the fingerboard, enabling vibrato and +microtonal inflections. + +Other String Instruments +------------------------ + +.. code-block:: python + + mandolin = Fretboard.mandolin() # E5 A4 D4 G3 (violin tuning) + banjo = Fretboard.banjo() # Open G (with high drone string) + banjo_d = Fretboard.banjo("open d") # Open D (clawhammer) + twelve = Fretboard.twelve_string() # 12 strings (6 doubled courses) + Custom Instruments ------------------ -Any fretted instrument can be modeled, including `banjo `_, -`mandolin `_, and more: +Any stringed instrument can be modeled: .. code-block:: python from pytheory import Tone, Fretboard - # Banjo (open G tuning) - banjo = Fretboard(tones=[ - Tone.from_string("D4"), + # Mandola (octave below mandolin, like viola to violin) + mandola = Fretboard(tones=[ + Tone.from_string("E4"), + Tone.from_string("A3"), + Tone.from_string("D3"), + Tone.from_string("G2"), + ]) + + # Baritone ukulele (DGBE — like the top 4 guitar strings) + bari_uke = Fretboard(tones=[ + Tone.from_string("E4"), Tone.from_string("B3"), Tone.from_string("G3"), Tone.from_string("D3"), - Tone.from_string("G4"), # 5th string (high drone) ]) - # Mandolin - mandolin = Fretboard(tones=[ - Tone.from_string("E5"), - Tone.from_string("A4"), - Tone.from_string("D4"), - Tone.from_string("G3"), + # Double bass / upright bass + upright = Fretboard(tones=[ + Tone.from_string("G2"), + Tone.from_string("D2"), + Tone.from_string("A1"), + Tone.from_string("E1"), ]) Getting Fingerings diff --git a/pytheory/chords.py b/pytheory/chords.py index 9ac9251..e089e46 100644 --- a/pytheory/chords.py +++ b/pytheory/chords.py @@ -589,7 +589,10 @@ class Fretboard: @classmethod def ukulele(cls): - """Standard ukulele tuning (A4 E4 C4 G4).""" + """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"), @@ -598,6 +601,125 @@ class Fretboard: Tone.from_string("G4", system="western"), ]) + @classmethod + def mandolin(cls): + """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"), + ]) + + @classmethod + def mandola(cls): + """Standard mandola tuning (A4 D4 G3 C3). + + An octave below the mandolin, same relationship as viola to + violin. Tuned in fifths: C3-G3-D4-A4. + """ + 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"), + ]) + + @classmethod + def violin(cls): + """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"), + ]) + + @classmethod + def viola(cls): + """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"), + ]) + + @classmethod + def cello(cls): + """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"), + ]) + + @classmethod + def banjo(cls, tuning="open g"): + """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. + + Standard open G: G4 D3 G3 B3 D4 (5th string is the short + high G4 drone). + """ + from .tones import Tone + tunings = { + "open g": ("D4", "B3", "G3", "D3", "G4"), + "open d": ("D4", "A3", "F#3", "D3", "A4"), + "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]) + + @classmethod + def twelve_string(cls): + """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). + """ + from .tones import Tone + strings = [ + "E4", "E4", # 1st course (unison) + "B3", "B3", # 2nd course (unison) + "G4", "G3", # 3rd course (octave) + "D4", "D3", # 4th course (octave) + "A3", "A2", # 5th course (octave) + "E3", "E2", # 6th course (octave) + ] + return cls(tones=[Tone.from_string(t, system="western") for t in strings]) + def fingering(self, *positions): if not len(positions) == len(self.tones): raise ValueError( diff --git a/test_pytheory.py b/test_pytheory.py index c89edeb..8156ba3 100644 --- a/test_pytheory.py +++ b/test_pytheory.py @@ -1829,6 +1829,59 @@ def test_fretboard_tunings_dict(): assert len(fb) == 6, f"Tuning {name} should have 6 strings" +def test_fretboard_mandolin(): + fb = Fretboard.mandolin() + assert len(fb) == 4 + assert fb.tones[0].name == "E" + assert fb.tones[-1].name == "G" + + +def test_fretboard_violin(): + fb = Fretboard.violin() + assert len(fb) == 4 + names = [t.name for t in fb] + assert names == ["E", "A", "D", "G"] + + +def test_fretboard_viola(): + fb = Fretboard.viola() + assert len(fb) == 4 + names = [t.name for t in fb] + assert names == ["A", "D", "G", "C"] + + +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 + + +def test_fretboard_banjo(): + fb = Fretboard.banjo() + assert len(fb) == 5 + assert fb.tones[-1].name == "G" # high drone string + + +def test_fretboard_banjo_open_d(): + fb = Fretboard.banjo("open d") + assert len(fb) == 5 + + +def test_fretboard_twelve_string(): + fb = Fretboard.twelve_string() + assert len(fb) == 12 + + +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] + assert interval == 7, f"Strings {i} and {i+1} not a 5th apart" + + # ── Ergonomic integration tests ───────────────────────────────────────────── def test_ergonomic_workflow():