mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 890c3cfbe2 | |||
| 599a00f066 |
+1
-1
@@ -10,7 +10,7 @@ sys.modules["sounddevice"] = MagicMock()
|
||||
project = "PyTheory"
|
||||
copyright = "2026, Kenneth Reitz"
|
||||
author = "Kenneth Reitz"
|
||||
release = "0.3.0"
|
||||
release = "0.3.1"
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.3.0"
|
||||
__version__ = "0.3.1"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS
|
||||
|
||||
+61
-3
@@ -73,6 +73,31 @@ class Chord:
|
||||
return any(item == t.name for t in self.tones)
|
||||
return item in self.tones
|
||||
|
||||
def __add__(self, other):
|
||||
"""Merge two chords into one (layer their tones).
|
||||
|
||||
Example::
|
||||
|
||||
>>> c_major = Chord.from_tones("C", "E", "G")
|
||||
>>> g_bass = Chord.from_tones("G", octave=2)
|
||||
>>> slash = c_major + g_bass # C/G
|
||||
"""
|
||||
if isinstance(other, Chord):
|
||||
return Chord(tones=list(self.tones) + list(other.tones))
|
||||
return NotImplemented
|
||||
|
||||
def tritone_sub(self):
|
||||
"""Return the tritone substitution of this chord.
|
||||
|
||||
In jazz harmony, any dominant chord can be replaced by the
|
||||
dominant chord a tritone (6 semitones) away. G7 → Db7,
|
||||
C7 → F#7. This works because the two chords share the same
|
||||
tritone interval (the 3rd and 7th swap roles).
|
||||
|
||||
Returns a new Chord transposed by 6 semitones.
|
||||
"""
|
||||
return self.transpose(6)
|
||||
|
||||
def inversion(self, n=1):
|
||||
"""Return the nth inversion of this chord.
|
||||
|
||||
@@ -599,6 +624,34 @@ class Fretboard:
|
||||
l = tuple([tone.full_name for tone in self.tones])
|
||||
return f"<Fretboard tones={l!r}>"
|
||||
|
||||
def capo(self, fret):
|
||||
"""Return a new Fretboard with a capo at the given fret.
|
||||
|
||||
A `capo <https://en.wikipedia.org/wiki/Capo>`_ clamps across
|
||||
all strings at a fret, raising every string's pitch by that
|
||||
many semitones. This lets you play open chord shapes in
|
||||
higher keys.
|
||||
|
||||
Common uses:
|
||||
|
||||
- Capo 2 + G shapes = A major voicings
|
||||
- Capo 4 + C shapes = E major voicings
|
||||
- Capo 7 + D shapes = A major voicings (bright, high register)
|
||||
|
||||
Example::
|
||||
|
||||
>>> fb = Fretboard.guitar(capo=2)
|
||||
>>> # Open strings are now F#4 C#4 A3 E3 B2 F#2
|
||||
>>> # Playing a "G shape" sounds as A major
|
||||
|
||||
Args:
|
||||
fret: The fret number to place the capo (1-12).
|
||||
|
||||
Returns:
|
||||
A new Fretboard with all strings raised by ``fret`` semitones.
|
||||
"""
|
||||
return Fretboard(tones=[t.add(fret) for t in self.tones])
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.tones)
|
||||
|
||||
@@ -627,18 +680,23 @@ class Fretboard:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def guitar(cls, tuning="standard"):
|
||||
"""Guitar with the given tuning.
|
||||
def guitar(cls, tuning="standard", capo=0):
|
||||
"""Guitar with the given tuning and optional capo.
|
||||
|
||||
Args:
|
||||
tuning: Tuning name or tuple of tone strings (high to low).
|
||||
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.
|
||||
"""
|
||||
from .tones import Tone
|
||||
if isinstance(tuning, str):
|
||||
tuning = cls.TUNINGS[tuning]
|
||||
return cls(tones=[Tone.from_string(t, system="western") for t in tuning])
|
||||
fb = cls(tones=[Tone.from_string(t, system="western") for t in tuning])
|
||||
if capo:
|
||||
fb = fb.capo(capo)
|
||||
return fb
|
||||
|
||||
@classmethod
|
||||
def bass(cls, five_string=False):
|
||||
|
||||
+68
-4
@@ -255,14 +255,26 @@ class Scale:
|
||||
|
||||
|
||||
PROGRESSIONS = {
|
||||
# Rock / Pop / Folk
|
||||
"I-IV-V-I": ("I", "IV", "V", "I"),
|
||||
"I-V-vi-IV": ("I", "V", "vi", "IV"),
|
||||
"ii-V-I": ("ii", "V7", "I"),
|
||||
"I-vi-IV-V": ("I", "vi", "IV", "V"),
|
||||
"12-bar blues": ("I", "I", "I", "I", "IV", "IV", "I", "I", "V", "IV", "I", "V"),
|
||||
"i-bVI-bIII-bVII": ("i", "VI", "III", "VII"),
|
||||
"vi-IV-I-V": ("vi", "IV", "I", "V"),
|
||||
"I-IV-vi-V": ("I", "IV", "vi", "V"),
|
||||
"vi-IV-I-V": ("vi", "IV", "I", "V"),
|
||||
# Blues
|
||||
"12-bar blues": ("I", "I", "I", "I", "IV", "IV", "I", "I", "V", "IV", "I", "V"),
|
||||
# Jazz
|
||||
"ii-V-I": ("ii", "V7", "I"),
|
||||
"I-vi-ii-V": ("I", "vi", "ii", "V"), # rhythm changes A section
|
||||
"iii-vi-ii-V": ("iii", "vi", "ii", "V"), # jazz turnaround
|
||||
# Classical / Film
|
||||
"i-bVI-bIII-bVII": ("i", "VI", "III", "VII"),
|
||||
"Pachelbel": ("I", "V", "vi", "iii", "IV", "I", "IV", "V"),
|
||||
# Flamenco / Spanish
|
||||
"Andalusian": ("i", "VII", "VI", "V"),
|
||||
# Modal
|
||||
"Dorian vamp": ("i", "IV"),
|
||||
"Mixolydian vamp": ("I", "VII"),
|
||||
}
|
||||
"""Common chord progressions as Roman numeral tuples.
|
||||
|
||||
@@ -396,6 +408,58 @@ class Key:
|
||||
"""
|
||||
return self._scale.nashville(*numbers)
|
||||
|
||||
def secondary_dominant(self, degree):
|
||||
"""Build a secondary dominant (V/x) for the given scale degree.
|
||||
|
||||
A secondary dominant is the dominant chord of a non-tonic
|
||||
degree. For example, in C major, V/V is D major (the V chord
|
||||
of G). Secondary dominants create momentary tonicizations
|
||||
that add color and forward motion.
|
||||
|
||||
Common secondary dominants:
|
||||
|
||||
- V/V (e.g. D7 in C major) — approaches the dominant
|
||||
- V/ii (e.g. A7 in C major) — approaches the supertonic
|
||||
- V/vi (e.g. E7 in C major) — approaches the relative minor
|
||||
|
||||
Args:
|
||||
degree: Scale degree to target (1-indexed). ``5`` means
|
||||
"build the V of the 5th degree."
|
||||
|
||||
Returns:
|
||||
A dominant 7th Chord that resolves to the given degree.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Key("C", "major").secondary_dominant(5) # V/V = D7
|
||||
<Chord D dominant 7th>
|
||||
"""
|
||||
target = self._scale.tones[degree - 1]
|
||||
# Build a dominant 7th a perfect 5th above the target
|
||||
from .chords import Chord
|
||||
root = target.add(7)
|
||||
return Chord(tones=[root, root.add(4), root.add(7), root.add(10)])
|
||||
|
||||
@classmethod
|
||||
def all_keys(cls):
|
||||
"""Return all 24 major and minor keys.
|
||||
|
||||
Returns:
|
||||
A list of Key objects for all 12 major and 12 minor keys.
|
||||
|
||||
Example::
|
||||
|
||||
>>> for k in Key.all_keys():
|
||||
... print(k)
|
||||
"""
|
||||
chromatic = ["C", "C#", "D", "D#", "E", "F",
|
||||
"F#", "G", "G#", "A", "A#", "B"]
|
||||
keys = []
|
||||
for tonic in chromatic:
|
||||
keys.append(cls(tonic, "major"))
|
||||
keys.append(cls(tonic, "minor"))
|
||||
return keys
|
||||
|
||||
@property
|
||||
def relative(self):
|
||||
"""The relative major or minor key.
|
||||
|
||||
+95
-1
@@ -2622,7 +2622,7 @@ def test_tension_empty():
|
||||
|
||||
def test_version():
|
||||
import pytheory
|
||||
assert pytheory.__version__ == "0.3.0"
|
||||
assert pytheory.__version__ == "0.3.1"
|
||||
|
||||
|
||||
def test_all_exports():
|
||||
@@ -3248,3 +3248,97 @@ def test_nashville_on_scale():
|
||||
prog = scale.nashville(1, 5, 1)
|
||||
assert prog[0].identify() == "C major"
|
||||
assert prog[1].identify() == "G major"
|
||||
|
||||
|
||||
# ── Capo ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_guitar_capo():
|
||||
fb = Fretboard.guitar(capo=2)
|
||||
assert fb.tones[0].name == "F#"
|
||||
assert len(fb) == 6
|
||||
|
||||
|
||||
def test_capo_method():
|
||||
fb = Fretboard.guitar()
|
||||
fb3 = fb.capo(3)
|
||||
assert fb3.tones[0].name == "G"
|
||||
|
||||
|
||||
def test_capo_zero():
|
||||
fb = Fretboard.guitar(capo=0)
|
||||
assert fb.tones[0].name == "E"
|
||||
|
||||
|
||||
# ── Chord.__add__ ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_chord_add():
|
||||
c = Chord.from_tones("C", "E", "G")
|
||||
bass = Chord.from_tones("G", octave=2)
|
||||
merged = c + bass
|
||||
assert len(merged) == 4
|
||||
|
||||
|
||||
def test_chord_add_preserves_tones():
|
||||
a = Chord.from_tones("C", "E")
|
||||
b = Chord.from_tones("G", "B")
|
||||
merged = a + b
|
||||
names = [t.name for t in merged]
|
||||
assert "C" in names and "G" in names
|
||||
|
||||
|
||||
# ── Tritone substitution ──────────────────────────────────────────────────
|
||||
|
||||
def test_tritone_sub():
|
||||
g7 = Chord.from_name("G7")
|
||||
sub = g7.tritone_sub()
|
||||
assert sub.identify() == "C# dominant 7th"
|
||||
|
||||
|
||||
def test_tritone_sub_is_6_semitones():
|
||||
c = Chord.from_tones("C", "E", "G")
|
||||
sub = c.tritone_sub()
|
||||
assert sub.root.name == "F#"
|
||||
|
||||
|
||||
# ── Secondary dominants ──────────────────────────────────────────────────
|
||||
|
||||
def test_secondary_dominant_V_of_V():
|
||||
k = Key("C", "major")
|
||||
vv = k.secondary_dominant(5)
|
||||
assert vv.identify() == "D dominant 7th"
|
||||
|
||||
|
||||
def test_secondary_dominant_V_of_ii():
|
||||
k = Key("C", "major")
|
||||
assert k.secondary_dominant(2).identify() == "A dominant 7th"
|
||||
|
||||
|
||||
def test_secondary_dominant_V_of_vi():
|
||||
k = Key("C", "major")
|
||||
assert k.secondary_dominant(6).identify() == "E dominant 7th"
|
||||
|
||||
|
||||
# ── Key.all_keys ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_all_keys():
|
||||
keys = Key.all_keys()
|
||||
assert len(keys) == 24
|
||||
majors = [k for k in keys if k.mode == "major"]
|
||||
minors = [k for k in keys if k.mode == "minor"]
|
||||
assert len(majors) == 12
|
||||
assert len(minors) == 12
|
||||
|
||||
|
||||
# ── More progressions ───────────────────────────────────────────────────
|
||||
|
||||
def test_progressions_count():
|
||||
from pytheory.scales import PROGRESSIONS
|
||||
assert len(PROGRESSIONS) >= 14
|
||||
|
||||
|
||||
def test_pachelbel_progression():
|
||||
from pytheory.scales import PROGRESSIONS
|
||||
k = Key("C", "major")
|
||||
prog = k.progression(*PROGRESSIONS["Pachelbel"])
|
||||
assert len(prog) == 8
|
||||
assert prog[0].identify() == "C major"
|
||||
|
||||
Reference in New Issue
Block a user