mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5aed586187 | |||
| 09d90b3425 | |||
| 96131da59c | |||
| d2058668a6 | |||
| a5ffdc6104 | |||
| 724a0df7b5 | |||
| 4750061b87 | |||
| d53d8b60dd | |||
| de1db0aa8d | |||
| b22b3c063f |
+158
-146
@@ -45,18 +45,20 @@ For seventh chords, there's also **third inversion** (7th in bass):
|
||||
|
||||
- G7 in third inversion: F G B D (notated G7/F)
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
# All three are "C major" — identify() finds the root
|
||||
root = Chord([Tone.from_string(n, system="western") for n in ["C4", "E4", "G4"]])
|
||||
first = Chord([Tone.from_string(n, system="western") for n in ["E3", "G3", "C4"]])
|
||||
second = Chord([Tone.from_string(n, system="western") for n in ["G3", "C4", "E4"]])
|
||||
>>> root = Chord([Tone.from_string(n, system="western") for n in ["C4", "E4", "G4"]])
|
||||
>>> first = Chord([Tone.from_string(n, system="western") for n in ["E3", "G3", "C4"]])
|
||||
>>> second = Chord([Tone.from_string(n, system="western") for n in ["G3", "C4", "E4"]])
|
||||
|
||||
root.identify() # 'C major'
|
||||
first.identify() # 'C major'
|
||||
second.identify() # 'C major'
|
||||
>>> root.identify()
|
||||
'C major'
|
||||
>>> first.identify()
|
||||
'C major'
|
||||
>>> second.identify()
|
||||
'C major'
|
||||
|
||||
Extended Chords
|
||||
---------------
|
||||
@@ -72,33 +74,42 @@ A full 13th chord contains all 7 notes of the scale! In practice,
|
||||
tones are usually omitted — the 5th is typically dropped first, then
|
||||
the 11th (which clashes with the 3rd in dominant chords).
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
scale = TonedScale(tonic="C4")["major"]
|
||||
>>> scale = TonedScale(tonic="C4")["major"]
|
||||
|
||||
# Build a Cmaj9 from the scale: C E G B D
|
||||
cmaj9 = scale.chord(0, 2, 4, 6, 8)
|
||||
|
||||
# Build a full C13 (in theory): C E G B D F A
|
||||
c13 = scale.chord(0, 2, 4, 6, 8, 10, 12)
|
||||
>>> cmaj9 = scale.chord(0, 2, 4, 6, 8)
|
||||
>>> c13 = scale.chord(0, 2, 4, 6, 8, 10, 12)
|
||||
|
||||
Using the Chord Chart
|
||||
---------------------
|
||||
|
||||
PyTheory includes 144 pre-built chords (12 roots x 12 qualities):
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import CHARTS
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
chart = CHARTS["western"]
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("C")
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
>>> fb.chord("Am")
|
||||
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
|
||||
>>> fb.chord("G7")
|
||||
Fingering(e=1, B=0, G=0, D=0, A=2, E=3)
|
||||
|
||||
c_major = chart["C"] # C major (root position)
|
||||
a_minor = chart["Am"] # A minor
|
||||
g_seven = chart["G7"] # G dominant 7th
|
||||
d_dim = chart["Ddim"] # D diminished
|
||||
You can also build chords directly with ``Chord.from_name()``:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> Chord.from_name("G7").identify()
|
||||
'G dominant 7th'
|
||||
>>> Chord.from_name("Ddim").identify()
|
||||
'D diminished'
|
||||
|
||||
Available qualities:
|
||||
|
||||
@@ -119,52 +130,48 @@ Quality Intervals Example tones (from C)
|
||||
``"maj9"`` 4, 7, 11, 14 C E G B D (major 9th)
|
||||
============ ================ ================================
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import CHARTS
|
||||
>>> chart = CHARTS["western"]
|
||||
|
||||
>>> chart["C"].acceptable_tone_names
|
||||
('C', 'E', 'G')
|
||||
|
||||
>>> chart["Cm7"].acceptable_tone_names
|
||||
('C', 'D#', 'G', 'A#') # Eb and Bb shown as sharps
|
||||
('C', 'Eb', 'G', 'Bb')
|
||||
|
||||
Building Chords
|
||||
---------------
|
||||
|
||||
Several convenience constructors make chord creation concise:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord
|
||||
>>> from pytheory import Chord
|
||||
|
||||
# From note names (simplest)
|
||||
Chord.from_tones("C", "E", "G") # <Chord C major>
|
||||
Chord.from_tones("A", "C", "E") # <Chord A minor>
|
||||
>>> Chord.from_tones("C", "E", "G").identify()
|
||||
'C major'
|
||||
>>> Chord.from_tones("A", "C", "E").identify()
|
||||
'A minor'
|
||||
|
||||
# From a chord name (uses the built-in chart)
|
||||
Chord.from_name("Am7") # <Chord A minor 7th>
|
||||
Chord.from_name("G7") # <Chord G dominant 7th>
|
||||
>>> Chord.from_name("Am7").identify()
|
||||
'A minor 7th'
|
||||
>>> Chord.from_name("G7").identify()
|
||||
'G dominant 7th'
|
||||
|
||||
# From root + semitone intervals
|
||||
Chord.from_intervals("C", 4, 7) # <Chord C major>
|
||||
Chord.from_intervals("D", 3, 7) # <Chord D minor>
|
||||
Chord.from_intervals("G", 4, 7, 10) # <Chord G dominant 7th>
|
||||
>>> Chord.from_intervals("C", 4, 7).identify()
|
||||
'C major'
|
||||
>>> Chord.from_intervals("G", 4, 7, 10).identify()
|
||||
'G dominant 7th'
|
||||
|
||||
# From MIDI note numbers
|
||||
Chord.from_midi_message(60, 64, 67) # <Chord C major>
|
||||
>>> Chord.from_midi_message(60, 64, 67).identify()
|
||||
'C major'
|
||||
|
||||
# Full manual construction
|
||||
from pytheory import Tone
|
||||
c_major = Chord(tones=[
|
||||
Tone.from_string("C4", system="western"),
|
||||
Tone.from_string("E4", system="western"),
|
||||
Tone.from_string("G4", system="western"),
|
||||
])
|
||||
|
||||
for tone in c_major:
|
||||
print(tone)
|
||||
|
||||
len(c_major) # 3
|
||||
"C" in c_major # True
|
||||
>>> len(Chord.from_name("C"))
|
||||
3
|
||||
>>> "C" in Chord.from_name("C")
|
||||
True
|
||||
|
||||
Intervals
|
||||
---------
|
||||
@@ -172,13 +179,13 @@ Intervals
|
||||
The ``intervals`` property returns semitone distances between adjacent
|
||||
tones — these are musically meaningful and octave-invariant:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c_major.intervals
|
||||
[4, 3] # major 3rd (4) + minor 3rd (3) = major triad
|
||||
>>> Chord.from_tones("C", "E", "G").intervals
|
||||
[4, 3]
|
||||
|
||||
>>> Chord(tones=[C4, Eb4, G4]).intervals
|
||||
[3, 4] # minor 3rd + major 3rd = minor triad
|
||||
>>> Chord.from_tones("C", "Eb", "G").intervals
|
||||
[3, 4]
|
||||
|
||||
Consonance and Dissonance
|
||||
-------------------------
|
||||
@@ -205,13 +212,16 @@ Minor 3rd 6:5 Every 6th wave aligns
|
||||
Tritone 45:32 Waves rarely align
|
||||
=========== ===== ====================
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
fifth = Chord([C4, G4])
|
||||
tritone = Chord([C4, F_sharp_4])
|
||||
>>> from pytheory import Chord, Tone
|
||||
>>> C4 = Tone.from_string("C4", system="western")
|
||||
>>> G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
fifth.harmony > tritone.harmony # True
|
||||
# The perfect fifth's 3:2 ratio scores higher
|
||||
>>> fifth = Chord([C4, G4])
|
||||
>>> tritone = Chord([C4, C4 + 6])
|
||||
>>> fifth.harmony > tritone.harmony
|
||||
True
|
||||
|
||||
Dissonance Score
|
||||
~~~~~~~~~~~~~~~~
|
||||
@@ -227,14 +237,13 @@ The roughness depends on the frequency difference relative to the
|
||||
that register). Maximum roughness occurs when the difference equals
|
||||
the critical bandwidth.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
# Octave: frequencies far apart → low roughness
|
||||
octave = Chord([C4, C5])
|
||||
# Major 3rd: closer frequencies → higher roughness
|
||||
third = Chord([C4, E4])
|
||||
|
||||
octave.dissonance < third.dissonance # True
|
||||
>>> E4 = Tone.from_string("E4", system="western")
|
||||
>>> octave = Chord([C4, C4 + 12])
|
||||
>>> third = Chord([C4, E4])
|
||||
>>> octave.dissonance < third.dissonance
|
||||
True
|
||||
|
||||
Beat Frequencies
|
||||
~~~~~~~~~~~~~~~~
|
||||
@@ -247,23 +256,23 @@ you hear a pulsing at the **beat frequency**: ``|f1 - f2|`` Hz.
|
||||
- **15–30 Hz**: Perceived as buzzing/roughness
|
||||
- **> 30 Hz**: No longer beating — becomes part of the timbre
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
chord = Chord(tones=[A4, E5, A5])
|
||||
>>> A4 = Tone.from_string("A4", system="western")
|
||||
>>> chord = Chord([A4, A4 + 7, A4 + 12])
|
||||
|
||||
# All pairwise beat frequencies, sorted ascending
|
||||
chord.beat_frequencies
|
||||
# [(A4, E5, 189.6), (E5, A5, 220.0), (A4, A5, 440.0)]
|
||||
>>> chord.beat_frequencies
|
||||
[...]
|
||||
|
||||
# The slowest (most perceptible) beat
|
||||
chord.beat_pulse # 189.6 Hz
|
||||
>>> round(chord.beat_pulse, 1)
|
||||
219.3
|
||||
|
||||
Transposition
|
||||
-------------
|
||||
|
||||
Shift an entire chord up or down by any number of semitones:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_name("C").transpose(7).identify()
|
||||
'G major'
|
||||
@@ -276,20 +285,20 @@ Chord Manipulation
|
||||
|
||||
Add or remove individual tones from a chord:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
c_major = Chord.from_tones("C", "E", "G")
|
||||
>>> c_major = Chord.from_tones("C", "E", "G")
|
||||
|
||||
# Add a tone to build a seventh chord
|
||||
b4 = Tone.from_string("B4", system="western")
|
||||
cmaj7 = c_major.add_tone(b4)
|
||||
cmaj7.identify() # 'C major 7th'
|
||||
>>> b4 = Tone.from_string("B4", system="western")
|
||||
>>> cmaj7 = c_major.add_tone(b4)
|
||||
>>> cmaj7.identify()
|
||||
'C major 7th'
|
||||
|
||||
# Remove a tone
|
||||
c_again = cmaj7.remove_tone("B")
|
||||
c_again.identify() # 'C major'
|
||||
>>> c_again = cmaj7.remove_tone("B")
|
||||
>>> c_again.identify()
|
||||
'C major'
|
||||
|
||||
Chord Identification
|
||||
--------------------
|
||||
@@ -298,27 +307,30 @@ Give PyTheory any set of tones and it will tell you what chord it is.
|
||||
It tries every tone as a potential root and matches the interval pattern
|
||||
against 17 known chord types (triads, 7ths, 9ths, sus, power chords).
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord
|
||||
>>> from pytheory import Chord
|
||||
|
||||
# From note names
|
||||
Chord.from_tones("A", "C", "E").identify() # 'A minor'
|
||||
Chord.from_tones("G", "B", "D", "F").identify() # 'G dominant 7th'
|
||||
>>> Chord.from_tones("A", "C", "E").identify()
|
||||
'A minor'
|
||||
>>> Chord.from_tones("G", "B", "D", "F").identify()
|
||||
'G dominant 7th'
|
||||
|
||||
# Works with any voicing or inversion
|
||||
Chord.from_tones("E", "G", "C").identify() # 'C major'
|
||||
>>> Chord.from_tones("E", "G", "C").identify()
|
||||
'C major'
|
||||
|
||||
# Flats work too
|
||||
Chord.from_tones("Bb", "D", "F").identify() # 'Bb major'
|
||||
>>> Chord.from_tones("Bb", "D", "F").identify()
|
||||
'Bb major'
|
||||
|
||||
You can also access the root and quality separately:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
chord = Chord.from_name("Am7")
|
||||
chord.root # <Tone A4>
|
||||
chord.quality # 'minor 7th'
|
||||
>>> chord = Chord.from_name("Am7")
|
||||
>>> chord.root
|
||||
<Tone A4>
|
||||
>>> chord.quality
|
||||
'minor 7th'
|
||||
|
||||
Harmonic Analysis
|
||||
-----------------
|
||||
@@ -328,22 +340,22 @@ key. This is how musicians describe chord progressions independent of
|
||||
key — "I-IV-V" means the same thing in C major (C-F-G) as in G major
|
||||
(G-C-D).
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
C4 = Tone.from_string("C4", system="western")
|
||||
D4 = Tone.from_string("D4", system="western")
|
||||
E4 = Tone.from_string("E4", system="western")
|
||||
F4 = Tone.from_string("F4", system="western")
|
||||
G4 = Tone.from_string("G4", system="western")
|
||||
A4 = Tone.from_string("A4", system="western")
|
||||
B4 = Tone.from_string("B4", system="western")
|
||||
>>> C4 = Tone.from_string("C4", system="western")
|
||||
>>> E4 = Tone.from_string("E4", system="western")
|
||||
>>> G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
Chord([C4, E4, G4]).analyze("C") # 'I' (tonic)
|
||||
Chord([D4, F4, A4]).analyze("C") # 'ii' (supertonic minor)
|
||||
Chord([G4, B4, G4+5]).analyze("C") # 'V' (dominant)
|
||||
Chord([G4, B4, G4+5, G4+10]).analyze("C") # 'V7' (dominant 7th)
|
||||
>>> Chord([C4, E4, G4]).analyze("C")
|
||||
'I'
|
||||
>>> Chord.from_tones("D", "F", "A").analyze("C")
|
||||
'ii'
|
||||
>>> Chord([G4, G4+4, G4+7]).analyze("C")
|
||||
'V'
|
||||
>>> Chord([G4, G4+4, G4+7, G4+10]).analyze("C")
|
||||
'V7'
|
||||
|
||||
Tension and Resolution
|
||||
----------------------
|
||||
@@ -359,18 +371,21 @@ quantifies this based on:
|
||||
- **Dominant function**: the specific combination of a major 3rd and
|
||||
minor 7th above the root — the hallmark of the V7 chord.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
# A C major triad is fully resolved — no tension
|
||||
c_major = Chord([C4, E4, G4])
|
||||
c_major.tension['score'] # 0.0
|
||||
c_major.tension['tritones'] # 0
|
||||
>>> c_major = Chord([C4, E4, G4])
|
||||
>>> c_major.tension['score']
|
||||
0.0
|
||||
>>> c_major.tension['tritones']
|
||||
0
|
||||
|
||||
# G7 is loaded with tension — it wants to resolve to C
|
||||
g7 = Chord([G4, B4, G4+5, G4+10])
|
||||
g7.tension['score'] # 0.6
|
||||
g7.tension['tritones'] # 1
|
||||
g7.tension['has_dominant_function'] # True
|
||||
>>> g7 = Chord([G4, G4+4, G4+7, G4+10])
|
||||
>>> g7.tension['score']
|
||||
0.6
|
||||
>>> g7.tension['tritones']
|
||||
1
|
||||
>>> g7.tension['has_dominant_function']
|
||||
True
|
||||
|
||||
Voice Leading
|
||||
-------------
|
||||
@@ -380,14 +395,16 @@ jumping all voices to new positions, good voice leading moves each note
|
||||
the minimum distance to reach the next chord. Bach's chorales are the
|
||||
gold standard — every voice moves by step whenever possible.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
c_maj = Chord([C4, E4, G4])
|
||||
f_maj = Chord([F4, A4, C4+12])
|
||||
>>> c_maj = Chord.from_tones("C", "E", "G")
|
||||
>>> f_maj = Chord.from_tones("F", "A", "C")
|
||||
|
||||
for src, dst, motion in c_maj.voice_leading(f_maj):
|
||||
print(f"{src} -> {dst} ({motion:+d} semitones)")
|
||||
# Each voice moves the minimum distance to reach the target chord
|
||||
>>> for src, dst, motion in c_maj.voice_leading(f_maj):
|
||||
... print(f"{src} -> {dst} ({motion:+d} semitones)")
|
||||
G4 -> A4 (+2 semitones)
|
||||
E4 -> F4 (+1 semitones)
|
||||
C4 -> C4 (+0 semitones)
|
||||
|
||||
Tritone Substitution
|
||||
--------------------
|
||||
@@ -400,17 +417,14 @@ tritone interval — the 3rd and 7th simply swap roles.
|
||||
|
||||
Common tritone subs: G7 <-> Db7, C7 <-> F#7, D7 <-> Ab7.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord
|
||||
>>> from pytheory import Chord
|
||||
|
||||
g7 = Chord.from_name("G7")
|
||||
sub = g7.tritone_sub()
|
||||
sub.identify() # 'C# dominant 7th' (enharmonic Db7)
|
||||
|
||||
# Both resolve to C — try them in a ii-V-I:
|
||||
# Dm7 → G7 → Cmaj7 (standard)
|
||||
# Dm7 → Db7 → Cmaj7 (with tritone sub — chromatic bass line!)
|
||||
>>> g7 = Chord.from_name("G7")
|
||||
>>> sub = g7.tritone_sub()
|
||||
>>> sub.identify()
|
||||
'C# dominant 7th'
|
||||
|
||||
The Overtone Series
|
||||
-------------------
|
||||
@@ -425,12 +439,10 @@ overtones of C already contain G. The two tones share acoustic energy,
|
||||
reinforcing each other. A dissonant interval like C and C# shares
|
||||
almost no overtones — the waves clash.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Tone
|
||||
>>> from pytheory import Tone
|
||||
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
a4.overtones(8)
|
||||
# [440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0, 3080.0, 3520.0]
|
||||
# A4 A5 E6 A6 C#7 E7 ~G7 A7
|
||||
# fund. oct. 5th+oct 2oct 3rd 5th ~7th 3oct
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> [round(f, 1) for f in a4.overtones(8)]
|
||||
[440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0, 3080.0, 3520.0]
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
Cookbook
|
||||
=======
|
||||
|
||||
Real-world recipes for common musical tasks. Each recipe is self-contained
|
||||
and ready to paste into a Python session.
|
||||
|
||||
Analyze a Song
|
||||
--------------
|
||||
|
||||
Take the chord progression from "Let It Be" (C G Am F) and analyze it
|
||||
in the key of C major:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord, Key
|
||||
|
||||
>>> C = Chord.from_name("C")
|
||||
>>> G = Chord.from_name("G")
|
||||
>>> Am = Chord.from_name("Am")
|
||||
>>> F = Chord.from_name("F")
|
||||
|
||||
>>> [c.identify() for c in [C, G, Am, F]]
|
||||
['C major', 'G major', 'A minor', 'F major']
|
||||
|
||||
>>> [c.analyze("C") for c in [C, G, Am, F]]
|
||||
['I', 'V', 'vi', 'IV']
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> [c.identify() for c in key.progression("I", "V", "vi", "IV")]
|
||||
['C major', 'G major', 'A minor', 'F major']
|
||||
|
||||
Write a 12-Bar Blues
|
||||
--------------------
|
||||
|
||||
The `12-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ is
|
||||
built from the I, IV, and V chords. Here it is in the key of A:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key, Chord
|
||||
|
||||
>>> key = Key("A", "major")
|
||||
>>> [c.identify() for c in key.progression("I", "IV", "V")]
|
||||
['A major', 'D major', 'E major']
|
||||
|
||||
>>> bars = ["I","I","I","I", "IV","IV","I","I", "V","IV","I","V"]
|
||||
>>> [c.identify() for c in key.progression(*bars)]
|
||||
['A major', 'A major', 'A major', 'A major', 'D major', 'D major', 'A major', 'A major', 'E major', 'D major', 'A major', 'E major']
|
||||
|
||||
>>> Chord.from_name("A7").identify()
|
||||
'A dominant 7th'
|
||||
>>> Chord.from_name("D7").identify()
|
||||
'D dominant 7th'
|
||||
>>> Chord.from_name("E7").identify()
|
||||
'E dominant 7th'
|
||||
|
||||
Find Chords in a Key
|
||||
--------------------
|
||||
|
||||
The :class:`~pytheory.scales.Key` class builds diatonic chords for any
|
||||
key and lets you pull progressions by Roman numeral or Nashville number:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> key = Key("G", "major")
|
||||
>>> key.chords
|
||||
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
|
||||
|
||||
>>> [c.identify() for c in key.progression("I", "V", "vi", "IV")]
|
||||
['G major', 'D major', 'E minor', 'C major']
|
||||
|
||||
>>> [c.identify() for c in key.nashville(1, 5, 6, 4)]
|
||||
['G major', 'D major', 'E minor', 'C major']
|
||||
|
||||
Compare Scales
|
||||
--------------
|
||||
|
||||
Play the same tonic through different scales to hear how each mode
|
||||
reshapes the palette. The western modes share the same notes but start
|
||||
on different degrees; the blues scale adds the "blue note" (flat 5th):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
>>> c["major"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
>>> c["minor"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
|
||||
>>> c["dorian"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
|
||||
>>> c["mixolydian"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
>>> c_blues = TonedScale(tonic="C4", system="blues")
|
||||
>>> c_blues["blues"].note_names
|
||||
['C', 'Eb', 'F', 'Gb', 'G', 'Bb', 'C']
|
||||
|
||||
Guitar Chord Chart
|
||||
------------------
|
||||
|
||||
Generate fingerings for guitar and ukulele with
|
||||
:class:`~pytheory.tones.Fretboard`:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb.chord("C")
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
>>> fb.chord("G")
|
||||
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
|
||||
>>> fb.chord("Am")
|
||||
Fingering(e=0, B=1, G=2, D=2, A=0, E=x)
|
||||
>>> fb.chord("D")
|
||||
Fingering(e=2, B=3, G=2, D=0, A=x, E=x)
|
||||
|
||||
>>> uke = Fretboard.ukulele()
|
||||
>>> uke.chord("C")
|
||||
Fingering(A=3, E=0, C=0, G=0)
|
||||
>>> uke.chord("G")
|
||||
Fingering(A=2, E=3, C=2, G=0)
|
||||
|
||||
Explore an Interval
|
||||
-------------------
|
||||
|
||||
Start from A4 (440 Hz) and walk through intervals, checking names and
|
||||
frequency ratios:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.frequency
|
||||
440.0
|
||||
|
||||
>>> minor_3rd = a4 + 3
|
||||
>>> a4.interval_to(minor_3rd)
|
||||
'minor 3rd'
|
||||
|
||||
>>> p5 = a4 + 7
|
||||
>>> a4.interval_to(p5)
|
||||
'perfect 5th'
|
||||
>>> round(p5.frequency / a4.frequency, 4)
|
||||
1.4983
|
||||
|
||||
>>> octave = a4 + 12
|
||||
>>> a4.interval_to(octave)
|
||||
'octave'
|
||||
>>> round(octave.frequency / a4.frequency, 4)
|
||||
2.0
|
||||
|
||||
Walk the Circle of Fifths
|
||||
-------------------------
|
||||
|
||||
The `circle of fifths <https://en.wikipedia.org/wiki/Circle_of_fifths>`_
|
||||
is the backbone of Western harmony — each step adds one sharp or flat:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> c = Tone.from_string("C4", system="western")
|
||||
>>> [t.name for t in c.circle_of_fifths()]
|
||||
['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
|
||||
|
||||
>>> g = Tone.from_string("G4", system="western")
|
||||
>>> [t.name for t in g.circle_of_fifths()]
|
||||
['G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F', 'C']
|
||||
|
||||
Voice Leading Between Chords
|
||||
-----------------------------
|
||||
|
||||
Find the smoothest path from one chord to the next — each voice moves
|
||||
the minimum distance:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> c_maj = Chord.from_tones("C", "E", "G")
|
||||
>>> f_maj = Chord.from_tones("F", "A", "C")
|
||||
|
||||
>>> for src, dst, motion in c_maj.voice_leading(f_maj):
|
||||
... print(f"{src} -> {dst} ({motion:+d} semitones)")
|
||||
G4 -> A4 (+2 semitones)
|
||||
E4 -> F4 (+1 semitones)
|
||||
C4 -> C4 (+0 semitones)
|
||||
|
||||
Measure Harmonic Tension
|
||||
------------------------
|
||||
|
||||
Quantify how much a chord "wants to resolve." Dominant 7ths have
|
||||
the most tension — the tritone between the 3rd and 7th pulls toward
|
||||
resolution:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> for name in ["C", "Am", "G7", "Cmaj7"]:
|
||||
... ch = Chord.from_name(name)
|
||||
... t = ch.tension
|
||||
... print(f"{name:6s} tension={t['score']:.2f} tritones={t['tritones']} dominant={t['has_dominant_function']}")
|
||||
C tension=0.00 tritones=0 dominant=False
|
||||
Am tension=0.00 tritones=0 dominant=False
|
||||
G7 tension=0.60 tritones=1 dominant=True
|
||||
Cmaj7 tension=0.15 tritones=0 dominant=False
|
||||
|
||||
Tritone Substitution (Jazz)
|
||||
---------------------------
|
||||
|
||||
Replace any dominant chord with the one a
|
||||
`tritone <https://en.wikipedia.org/wiki/Tritone_substitution>`_ away —
|
||||
they share the same tritone interval:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Chord
|
||||
|
||||
>>> g7 = Chord.from_name("G7")
|
||||
>>> g7.tritone_sub().identify()
|
||||
'C# dominant 7th'
|
||||
|
||||
>>> # ii-V-I with tritone sub:
|
||||
>>> # Dm7 -> G7 -> Cmaj7 (standard)
|
||||
>>> # Dm7 -> Db7 -> Cmaj7 (chromatic bass line!)
|
||||
|
||||
Key Signatures and Detection
|
||||
-----------------------------
|
||||
|
||||
View the accidentals in any key, or detect the key from a set of notes:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> Key("C", "major").signature
|
||||
{'sharps': 0, 'flats': 0, 'accidentals': []}
|
||||
>>> Key("G", "major").signature
|
||||
{'sharps': 1, 'flats': 0, 'accidentals': ['F#']}
|
||||
>>> Key("D", "major").signature
|
||||
{'sharps': 2, 'flats': 0, 'accidentals': ['F#', 'C#']}
|
||||
|
||||
>>> Key.detect("C", "E", "G", "A", "D")
|
||||
<Key C major>
|
||||
|
||||
Relative and Parallel Keys
|
||||
--------------------------
|
||||
|
||||
Every major key has a **relative minor** (same notes, different root)
|
||||
and a **parallel minor** (same root, different notes):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> c = Key("C", "major")
|
||||
>>> c.relative
|
||||
'A minor'
|
||||
>>> c.parallel
|
||||
'C minor'
|
||||
|
||||
Borrowed Chords and Secondary Dominants
|
||||
---------------------------------------
|
||||
|
||||
Add color by borrowing from the parallel key or building secondary
|
||||
dominants that approach other scale degrees:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> c = Key("C", "major")
|
||||
|
||||
>>> c.borrowed_chords[:4]
|
||||
['C minor', 'D diminished', 'Eb major', 'F minor']
|
||||
|
||||
>>> c.secondary_dominant(5).identify()
|
||||
'D dominant 7th'
|
||||
>>> c.secondary_dominant(2).identify()
|
||||
'A dominant 7th'
|
||||
>>> c.secondary_dominant(6).identify()
|
||||
'E dominant 7th'
|
||||
|
||||
The Overtone Series
|
||||
-------------------
|
||||
|
||||
Every musical tone contains a stack of harmonics — the physics behind
|
||||
why intervals sound consonant:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> [round(f, 1) for f in a4.overtones(6)]
|
||||
[440.0, 880.0, 1320.0, 1760.0, 2200.0, 2640.0]
|
||||
|
||||
>>> # Harmonic 2 = octave (2:1)
|
||||
>>> # Harmonic 3 = perfect 5th + octave (3:1)
|
||||
>>> # Harmonic 5 = major 3rd + two octaves (5:1)
|
||||
|
||||
Enharmonic Spellings
|
||||
--------------------
|
||||
|
||||
Find the alternate name for any sharp or flat:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Tone
|
||||
|
||||
>>> for name in ["C#4", "D#4", "F#4", "G#4"]:
|
||||
... t = Tone.from_string(name, system="western")
|
||||
... print(f"{t.name} = {t.enharmonic}")
|
||||
C# = Db
|
||||
D# = Eb
|
||||
F# = Gb
|
||||
G# = Ab
|
||||
|
||||
World Scales
|
||||
------------
|
||||
|
||||
Explore scales from Indian, Arabic, and Japanese traditions:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> indian = TonedScale(tonic="Sa", system="indian")
|
||||
>>> indian["bhairav"].note_names
|
||||
['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
>>> arabic = TonedScale(tonic="Do", system="arabic")
|
||||
>>> arabic["hijaz"].note_names
|
||||
['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
|
||||
|
||||
>>> japanese = TonedScale(tonic="C4", system="japanese")
|
||||
>>> japanese["hirajoshi"].note_names
|
||||
['C', 'D', 'Eb', 'G', 'Ab', 'C']
|
||||
|
||||
Visualize a Scale on Guitar
|
||||
----------------------------
|
||||
|
||||
See where the notes fall across the fretboard — E minor pentatonic,
|
||||
the most-played scale in rock:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard, Scale
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> pent = Scale(tonic="E4", system="blues")["minor pentatonic"]
|
||||
>>> print(fb.scale_diagram(pent, frets=12))
|
||||
0 1 2 3 4 5 6 7 8 9 10 11 12
|
||||
E| E | - | - | G | - | A | - | B | - | - | D | - | E |
|
||||
B| B | - | - | D | - | E | - | - | G | - | A | - | B |
|
||||
G| G | - | A | - | B | - | - | D | - | E | - | - | G |
|
||||
D| D | - | E | - | - | G | - | A | - | B | - | - | D |
|
||||
A| A | - | B | - | - | D | - | E | - | - | G | - | A |
|
||||
E| E | - | - | G | - | A | - | B | - | - | D | - | E |
|
||||
+93
-103
@@ -31,42 +31,42 @@ Guitars
|
||||
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).
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Fretboard
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
guitar = Fretboard.guitar() # Standard EADGBE
|
||||
twelve = Fretboard.twelve_string() # 12-string (6 doubled courses)
|
||||
bass = Fretboard.bass() # Standard 4-string EADG
|
||||
bass5 = Fretboard.bass(five_string=True) # 5-string with low B
|
||||
>>> guitar = Fretboard.guitar() # Standard EADGBE
|
||||
>>> twelve = Fretboard.twelve_string() # 12-string (6 doubled courses)
|
||||
>>> bass = Fretboard.bass() # Standard 4-string EADG
|
||||
>>> bass5 = Fretboard.bass(five_string=True) # 5-string with low B
|
||||
|
||||
**Alternate tunings** — 8 built-in presets:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
Fretboard.guitar("drop d") # DADGBE — heavy riffs, metal
|
||||
Fretboard.guitar("open g") # DGDGBD — slide guitar, Keith Richards
|
||||
Fretboard.guitar("open d") # DADF#AD — slide, folk
|
||||
Fretboard.guitar("open e") # EBEG#BE — slide blues
|
||||
Fretboard.guitar("open a") # EAC#EAE
|
||||
Fretboard.guitar("dadgad") # DADGAD — Celtic, fingerstyle
|
||||
Fretboard.guitar("half step down") # Eb standard — Hendrix, SRV
|
||||
>>> Fretboard.guitar("drop d") # DADGBE — heavy riffs, metal
|
||||
>>> Fretboard.guitar("open g") # DGDGBD — slide guitar, Keith Richards
|
||||
>>> Fretboard.guitar("open d") # DADF#AD — slide, folk
|
||||
>>> Fretboard.guitar("open e") # EBEG#BE — slide blues
|
||||
>>> Fretboard.guitar("open a") # EAC#EAE
|
||||
>>> Fretboard.guitar("dadgad") # DADGAD — Celtic, fingerstyle
|
||||
>>> Fretboard.guitar("half step down") # Eb standard — Hendrix, SRV
|
||||
|
||||
# Custom tuning with any notes
|
||||
Fretboard.guitar(("C4", "G3", "C3", "G2", "C2", "G1"))
|
||||
>>> # Custom tuning with any notes
|
||||
>>> Fretboard.guitar(("C4", "G3", "C3", "G2", "C2", "G1"))
|
||||
|
||||
**Capo** — a `capo <https://en.wikipedia.org/wiki/Capo>`_ raises all
|
||||
strings by a number of frets, letting you play open chord shapes in
|
||||
higher keys:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
# Capo on fret 2 — open G shape now sounds as A major
|
||||
fb = Fretboard.guitar(capo=2)
|
||||
>>> # Capo on fret 2 — open G shape now sounds as A major
|
||||
>>> fb = Fretboard.guitar(capo=2)
|
||||
|
||||
# Or apply a capo to an existing fretboard
|
||||
fb = Fretboard.guitar()
|
||||
fb_capo3 = fb.capo(3)
|
||||
>>> # Or apply a capo to an existing fretboard
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb_capo3 = fb.capo(3)
|
||||
|
||||
The Mandolin Family
|
||||
-------------------
|
||||
@@ -76,12 +76,12 @@ mirrors the `violin family <https://en.wikipedia.org/wiki/Violin_family>`_
|
||||
— all tuned in perfect fifths, with each member a fifth or octave
|
||||
lower than the last:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
Fretboard.mandolin() # E5 A4 D4 G3 — soprano (= violin)
|
||||
Fretboard.mandola() # A4 D4 G3 C3 — alto (= viola)
|
||||
Fretboard.octave_mandolin() # E4 A3 D3 G2 — tenor (octave below mandolin)
|
||||
Fretboard.mandocello() # A3 D3 G2 C2 — bass (= cello)
|
||||
>>> Fretboard.mandolin() # E5 A4 D4 G3 — soprano (= violin)
|
||||
>>> Fretboard.mandola() # A4 D4 G3 C3 — alto (= viola)
|
||||
>>> Fretboard.octave_mandolin() # E4 A3 D3 G2 — tenor (octave below mandolin)
|
||||
>>> Fretboard.mandocello() # A3 D3 G2 C2 — bass (= cello)
|
||||
|
||||
The mandolin's doubled courses (pairs of strings) create a natural
|
||||
chorus effect. The `octave mandolin <https://en.wikipedia.org/wiki/Octave_mandolin>`_
|
||||
@@ -93,12 +93,12 @@ The Bowed String Family
|
||||
The orchestral `string family <https://en.wikipedia.org/wiki/String_section>`_
|
||||
is tuned in perfect fifths (except the double bass, which uses fourths):
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
Fretboard.violin() # E5 A4 D4 G3 — soprano
|
||||
Fretboard.viola() # A4 D4 G3 C3 — alto (5th below violin)
|
||||
Fretboard.cello() # A3 D3 G2 C2 — tenor/bass (octave below viola)
|
||||
Fretboard.double_bass() # G2 D2 A1 E1 — bass (tuned in 4ths!)
|
||||
>>> Fretboard.violin() # E5 A4 D4 G3 — soprano
|
||||
>>> Fretboard.viola() # A4 D4 G3 C3 — alto (5th below violin)
|
||||
>>> Fretboard.cello() # A3 D3 G2 C2 — tenor/bass (octave below viola)
|
||||
>>> Fretboard.double_bass() # G2 D2 A1 E1 — bass (tuned in 4ths!)
|
||||
|
||||
Bowed strings have no frets — the player can produce any pitch along
|
||||
the fingerboard, enabling continuous
|
||||
@@ -108,19 +108,19 @@ inflections not possible on fretted instruments.
|
||||
The `erhu <https://en.wikipedia.org/wiki/Erhu>`_ — a 2-stringed Chinese
|
||||
bowed instrument with a hauntingly vocal quality:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
Fretboard.erhu() # A4 D4 — tuned a 5th apart, no fingerboard
|
||||
>>> Fretboard.erhu() # A4 D4 — tuned a 5th apart, no fingerboard
|
||||
|
||||
Plucked Strings
|
||||
---------------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
Fretboard.ukulele() # A4 E4 C4 G4 — re-entrant tuning
|
||||
Fretboard.banjo() # Open G (bluegrass, 5th string is high drone)
|
||||
Fretboard.banjo("open d") # Open D (clawhammer, old-time)
|
||||
Fretboard.harp() # 47 strings, C1 to G7 (concert pedal harp)
|
||||
>>> Fretboard.ukulele() # A4 E4 C4 G4 — re-entrant tuning
|
||||
>>> Fretboard.banjo() # Open G (bluegrass, 5th string is high drone)
|
||||
>>> Fretboard.banjo("open d") # Open D (clawhammer, old-time)
|
||||
>>> Fretboard.harp() # 47 strings, C1 to G7 (concert pedal harp)
|
||||
|
||||
The `banjo <https://en.wikipedia.org/wiki/Banjo>`_'s short 5th string
|
||||
is a high drone — a defining feature of the instrument's sound.
|
||||
@@ -132,28 +132,28 @@ by up to two semitones across all octaves simultaneously.
|
||||
World Instruments
|
||||
-----------------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
# Middle Eastern
|
||||
Fretboard.oud() # C4 G3 D3 A2 G2 C2 — fretless, ancestor of the lute
|
||||
Fretboard.sitar() # 7 main strings — Indian classical
|
||||
>>> # Middle Eastern
|
||||
>>> Fretboard.oud() # C4 G3 D3 A2 G2 C2 — fretless, ancestor of the lute
|
||||
>>> Fretboard.sitar() # 7 main strings — Indian classical
|
||||
|
||||
# East Asian
|
||||
Fretboard.shamisen() # C4 G3 C3 — 3-string Japanese, honchoshi tuning
|
||||
Fretboard.pipa() # D4 A3 E3 A2 — 4-string Chinese lute
|
||||
Fretboard.erhu() # A4 D4 — 2-string Chinese bowed
|
||||
>>> # East Asian
|
||||
>>> Fretboard.shamisen() # C4 G3 C3 — 3-string Japanese, honchoshi tuning
|
||||
>>> Fretboard.pipa() # D4 A3 E3 A2 — 4-string Chinese lute
|
||||
>>> Fretboard.erhu() # A4 D4 — 2-string Chinese bowed
|
||||
|
||||
# European
|
||||
Fretboard.bouzouki() # D4 A3 D3 G2 — Irish (Celtic music)
|
||||
Fretboard.bouzouki("greek") # D4 A3 F3 C3 — Greek
|
||||
Fretboard.lute() # G4 D4 A3 F3 C3 G2 — Renaissance (6 courses)
|
||||
Fretboard.balalaika() # A4 E4 E4 — Russian (2 unison strings)
|
||||
>>> # European
|
||||
>>> Fretboard.bouzouki() # D4 A3 D3 G2 — Irish (Celtic music)
|
||||
>>> Fretboard.bouzouki("greek") # D4 A3 F3 C3 — Greek
|
||||
>>> Fretboard.lute() # G4 D4 A3 F3 C3 G2 — Renaissance (6 courses)
|
||||
>>> Fretboard.balalaika() # A4 E4 E4 — Russian (2 unison strings)
|
||||
|
||||
# Latin American
|
||||
Fretboard.charango() # E5 A4 E5 C5 G4 — Andean (re-entrant tuning)
|
||||
>>> # Latin American
|
||||
>>> Fretboard.charango() # E5 A4 E5 C5 G4 — Andean (re-entrant tuning)
|
||||
|
||||
# Steel guitar
|
||||
Fretboard.pedal_steel() # 10 strings, E9 Nashville — country music
|
||||
>>> # Steel guitar
|
||||
>>> Fretboard.pedal_steel() # 10 strings, E9 Nashville — country music
|
||||
|
||||
The `oud <https://en.wikipedia.org/wiki/Oud>`_ is fretless, allowing
|
||||
the quarter-tone inflections essential to
|
||||
@@ -164,12 +164,12 @@ sympathetic strings that resonate in harmony with the played notes.
|
||||
Keyboards
|
||||
---------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
Fretboard.keyboard() # 88-key piano (A0 to C8)
|
||||
Fretboard.keyboard(61, "C2") # 61-key synth controller
|
||||
Fretboard.keyboard(49, "C2") # 49-key controller
|
||||
Fretboard.keyboard(25, "C3") # 25-key mini MIDI controller
|
||||
>>> Fretboard.keyboard() # 88-key piano (A0 to C8)
|
||||
>>> Fretboard.keyboard(61, "C2") # 61-key synth controller
|
||||
>>> Fretboard.keyboard(49, "C2") # 49-key controller
|
||||
>>> Fretboard.keyboard(25, "C3") # 25-key mini MIDI controller
|
||||
|
||||
While keyboards don't have strings or frets, they map naturally to a
|
||||
sequence of tones. A full 88-key piano spans over 7 octaves — the
|
||||
@@ -187,12 +187,12 @@ on any instrument. It scores each possibility by:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard, CHARTS
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> f = fb.chord("C")
|
||||
>>> f
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=0)
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
|
||||
>>> f['A']
|
||||
3
|
||||
@@ -206,14 +206,6 @@ on any instrument. It scores each possibility by:
|
||||
>>> chord.identify()
|
||||
'C major'
|
||||
|
||||
>>> # All equally-scored fingerings via CHARTS
|
||||
>>> CHARTS["western"]["C"].fingering(fretboard=fb, multiple=True)
|
||||
[...]
|
||||
|
||||
>>> # Muted strings appear as None
|
||||
>>> CHARTS["western"]["F"].fingering(fretboard=fb)
|
||||
...
|
||||
|
||||
You can also go from fret positions to chord identification:
|
||||
|
||||
.. code-block:: pycon
|
||||
@@ -238,65 +230,63 @@ low E as ``E``::
|
||||
G|--0-- (open — G)
|
||||
D|--2-- (fret 2 — E)
|
||||
A|--3-- (fret 3 — C)
|
||||
E|--0-- (open — E)
|
||||
E|--x-- (muted)
|
||||
|
||||
A value of ``None`` means the string is muted (not played).
|
||||
A value of ``x`` (``None``) means the string is muted (not played).
|
||||
|
||||
ASCII Tablature
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
For a more visual representation, use ``tab()``:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> print(CHARTS["western"]["C"].tab(fretboard=fb))
|
||||
C
|
||||
E|--0--
|
||||
>>> print(fb.tab("C"))
|
||||
C major
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
A|--3--
|
||||
E|--0--
|
||||
E|--x--
|
||||
|
||||
Generating Full Charts
|
||||
----------------------
|
||||
|
||||
Generate fingerings for every chord at once:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Fretboard, charts_for_fretboard
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> chart = fb.chart()
|
||||
|
||||
fb = Fretboard.guitar()
|
||||
chart = charts_for_fretboard(fretboard=fb)
|
||||
>>> chart["C"]
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
|
||||
for name, fingering in chart.items():
|
||||
print(f"{name:6s} {fingering}")
|
||||
|
||||
# Works with any instrument
|
||||
uke_chart = charts_for_fretboard(fretboard=Fretboard.ukulele())
|
||||
mando_chart = charts_for_fretboard(fretboard=Fretboard.mandolin())
|
||||
>>> # Works with any instrument
|
||||
>>> uke_chart = Fretboard.ukulele().chart()
|
||||
>>> mando_chart = Fretboard.mandolin().chart()
|
||||
|
||||
Custom Instruments
|
||||
------------------
|
||||
|
||||
Any instrument can be modeled with custom string tunings:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Tone, Fretboard
|
||||
>>> from pytheory import Tone, Fretboard
|
||||
|
||||
# Baritone ukulele (DGBE — top 4 guitar strings)
|
||||
bari_uke = Fretboard(tones=[
|
||||
Tone.from_string("E4"),
|
||||
Tone.from_string("B3"),
|
||||
Tone.from_string("G3"),
|
||||
Tone.from_string("D3"),
|
||||
])
|
||||
>>> # Baritone ukulele (DGBE — top 4 guitar strings)
|
||||
>>> bari_uke = Fretboard(tones=[
|
||||
... Tone.from_string("E4"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("G3"),
|
||||
... Tone.from_string("D3"),
|
||||
... ])
|
||||
|
||||
# Tres cubano (Cuban guitar, 3 doubled courses)
|
||||
tres = Fretboard(tones=[
|
||||
Tone.from_string("E4"),
|
||||
Tone.from_string("B3"),
|
||||
Tone.from_string("G3"),
|
||||
])
|
||||
>>> # Tres cubano (Cuban guitar, 3 doubled courses)
|
||||
>>> tres = Fretboard(tones=[
|
||||
... Tone.from_string("E4"),
|
||||
... Tone.from_string("B3"),
|
||||
... Tone.from_string("G3"),
|
||||
... ])
|
||||
|
||||
+49
-29
@@ -13,25 +13,25 @@ using basic `waveform <https://en.wikipedia.org/wiki/Waveform>`_ synthesis.
|
||||
Playing a Tone
|
||||
--------------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Tone, play
|
||||
>>> from pytheory import Tone, play
|
||||
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
play(a4, t=1_000) # Play A440 for 1 second
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> play(a4, t=1_000) # Play A440 for 1 second
|
||||
|
||||
Playing a Chord
|
||||
---------------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord, play
|
||||
>>> from pytheory import Chord, play
|
||||
|
||||
# From a chord name
|
||||
play(Chord.from_name("Am7"), t=2_000)
|
||||
>>> # From a chord name
|
||||
>>> play(Chord.from_name("Am7"), t=2_000)
|
||||
|
||||
# From note names
|
||||
play(Chord.from_tones("C", "E", "G"), t=2_000)
|
||||
>>> # From note names
|
||||
>>> play(Chord.from_tones("C", "E", "G"), t=2_000)
|
||||
|
||||
Waveform Types
|
||||
--------------
|
||||
@@ -52,48 +52,68 @@ integer multiples of the fundamental frequency.
|
||||
1/n². Sounds softer and more mellow than sawtooth — somewhere between
|
||||
sine and sawtooth. Often described as "woody" or "hollow."
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import play, Synth, Tone
|
||||
>>> from pytheory import play, Synth, Tone
|
||||
|
||||
tone = Tone.from_string("C4", system="western")
|
||||
>>> tone = Tone.from_string("C4", system="western")
|
||||
|
||||
play(tone, synth=Synth.SINE) # Pure, clean
|
||||
play(tone, synth=Synth.SAW) # Bright, buzzy
|
||||
play(tone, synth=Synth.TRIANGLE) # Mellow, hollow
|
||||
>>> play(tone, synth=Synth.SINE) # Pure, clean
|
||||
>>> play(tone, synth=Synth.SAW) # Bright, buzzy
|
||||
>>> play(tone, synth=Synth.TRIANGLE) # Mellow, hollow
|
||||
|
||||
Temperaments
|
||||
------------
|
||||
|
||||
Hear the difference between tuning systems:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
play(tone, temperament="equal") # Modern standard (since ~1917)
|
||||
play(tone, temperament="pythagorean") # Pure fifths, wolf intervals
|
||||
play(tone, temperament="meantone") # Pure thirds, Renaissance sound
|
||||
>>> play(tone, temperament="equal") # Modern standard (since ~1917)
|
||||
>>> play(tone, temperament="pythagorean") # Pure fifths, wolf intervals
|
||||
>>> play(tone, temperament="meantone") # Pure thirds, Renaissance sound
|
||||
|
||||
Try playing a C major chord in each temperament — you'll hear subtle
|
||||
differences in the "color" of the major third. Equal temperament is
|
||||
a compromise; the other systems sacrifice some keys to make the good
|
||||
keys sound better.
|
||||
|
||||
Chord Progressions
|
||||
-------------------
|
||||
|
||||
Play an entire chord progression in sequence with a single call:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key, play_progression
|
||||
|
||||
>>> chords = Key("C", "major").progression("I", "V", "vi", "IV")
|
||||
>>> play_progression(chords, t=800)
|
||||
|
||||
You can customize the waveform and the gap (silence) between chords:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Synth
|
||||
|
||||
>>> play_progression(chords, t=1000, synth=Synth.TRIANGLE, gap=200)
|
||||
|
||||
Saving to WAV
|
||||
-------------
|
||||
|
||||
Render tones or chords to a WAV file instead of playing them live.
|
||||
This works even without speakers or PortAudio:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import save, Chord, Tone, Synth
|
||||
>>> from pytheory import save, Chord, Tone, Synth
|
||||
|
||||
# Save a single tone
|
||||
save(Tone.from_string("A4"), "a440.wav", t=1_000)
|
||||
>>> # Save a single tone
|
||||
>>> save(Tone.from_string("A4"), "a440.wav", t=1_000)
|
||||
|
||||
# Save a chord
|
||||
save(Chord.from_name("Am7"), "am7.wav", t=2_000)
|
||||
>>> # Save a chord
|
||||
>>> save(Chord.from_name("Am7"), "am7.wav", t=2_000)
|
||||
|
||||
# Choose waveform and temperament
|
||||
save(Chord.from_name("C"), "c_triangle.wav",
|
||||
synth=Synth.TRIANGLE, temperament="meantone", t=3_000)
|
||||
>>> # Choose waveform and temperament
|
||||
>>> save(Chord.from_name("C"), "c_triangle.wav",
|
||||
... synth=Synth.TRIANGLE, temperament="meantone", t=3_000)
|
||||
|
||||
+84
-70
@@ -19,58 +19,64 @@ Tones
|
||||
|
||||
A :class:`~pytheory.tones.Tone` is a single musical note:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Tone
|
||||
>>> from pytheory import Tone
|
||||
|
||||
# Create tones — sharps and flats both work
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
a4.frequency # 440.0 Hz — the tuning standard
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.frequency
|
||||
440.0
|
||||
|
||||
c4 = Tone.from_string("C4", system="western")
|
||||
c4.midi # 60 — middle C
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> c4.midi
|
||||
60
|
||||
|
||||
# From a frequency or MIDI number
|
||||
Tone.from_frequency(440) # <Tone A4>
|
||||
Tone.from_midi(60) # <Tone C4>
|
||||
>>> Tone.from_frequency(440)
|
||||
<Tone A4>
|
||||
>>> Tone.from_midi(60)
|
||||
<Tone C4>
|
||||
|
||||
# Tone arithmetic
|
||||
c4 + 4 # <Tone E4> — major third up
|
||||
c4 + 7 # <Tone G4> — perfect fifth up
|
||||
>>> c4 + 4
|
||||
<Tone E4>
|
||||
>>> c4 + 7
|
||||
<Tone G4>
|
||||
|
||||
# Interval between two tones
|
||||
g4 = c4 + 7
|
||||
g4 - c4 # 7 semitones
|
||||
c4.interval_to(g4) # 'perfect 5th'
|
||||
>>> g4 = c4 + 7
|
||||
>>> g4 - c4
|
||||
7
|
||||
>>> c4.interval_to(g4)
|
||||
'perfect 5th'
|
||||
|
||||
# Enharmonics
|
||||
Tone.from_string("C#4", system="western").enharmonic # 'Db'
|
||||
>>> Tone.from_string("C#4", system="western").enharmonic
|
||||
'Db'
|
||||
|
||||
Scales
|
||||
------
|
||||
|
||||
Build scales in any key and mode:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
|
||||
c["major"].note_names
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
>>> c["major"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
c["minor"].note_names
|
||||
# ['C', 'D', 'D#', 'F', 'G', 'G#', 'A#', 'C']
|
||||
>>> c["minor"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
|
||||
|
||||
c["dorian"].note_names
|
||||
# ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C']
|
||||
>>> c["dorian"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
# Access scale degrees by name or numeral
|
||||
major = c["major"]
|
||||
major["tonic"] # C4
|
||||
major["dominant"] # G4
|
||||
major["V"] # G4
|
||||
>>> major = c["major"]
|
||||
>>> major["tonic"]
|
||||
C4
|
||||
>>> major["dominant"]
|
||||
G4
|
||||
>>> major["V"]
|
||||
G4
|
||||
|
||||
Keys and Chords
|
||||
---------------
|
||||
@@ -78,41 +84,42 @@ Keys and Chords
|
||||
The :class:`~pytheory.scales.Key` class ties everything together —
|
||||
scales, chords, and progressions:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Key
|
||||
>>> from pytheory import Key
|
||||
|
||||
key = Key("G", "major")
|
||||
key.note_names # ['G', 'A', 'B', 'C', 'D', 'E', 'F#', 'G']
|
||||
>>> key = Key("G", "major")
|
||||
>>> key.note_names
|
||||
['G', 'A', 'B', 'C', 'D', 'E', 'F#', 'G']
|
||||
|
||||
# All diatonic triads
|
||||
key.chords
|
||||
# ['G major', 'A minor', 'B minor', 'C major',
|
||||
# 'D major', 'E minor', 'F# diminished']
|
||||
>>> key.chords
|
||||
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
|
||||
|
||||
# Build progressions from Roman numerals
|
||||
chords = key.progression("I", "V", "vi", "IV")
|
||||
[c.identify() for c in chords]
|
||||
# ['G major', 'D major', 'E minor', 'C major']
|
||||
>>> chords = key.progression("I", "V", "vi", "IV")
|
||||
>>> [c.identify() for c in chords]
|
||||
['G major', 'D major', 'E minor', 'C major']
|
||||
|
||||
# Detect the key from notes
|
||||
Key.detect("C", "E", "G", "A", "D") # <Key C major>
|
||||
>>> Key.detect("C", "E", "G", "A", "D")
|
||||
<Key C major>
|
||||
|
||||
Build chords directly:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord
|
||||
>>> from pytheory import Chord
|
||||
|
||||
Chord.from_tones("C", "E", "G") # <Chord C major>
|
||||
Chord.from_name("Am7") # <Chord A minor 7th>
|
||||
Chord.from_intervals("G", 4, 7, 10) # <Chord G dominant 7th>
|
||||
>>> Chord.from_tones("C", "E", "G")
|
||||
<Chord C major>
|
||||
>>> Chord.from_name("Am7")
|
||||
<Chord A minor 7th>
|
||||
>>> Chord.from_intervals("G", 4, 7, 10)
|
||||
<Chord G dominant 7th>
|
||||
|
||||
# Identify any chord
|
||||
Chord.from_tones("Bb", "D", "F").identify() # 'Bb major'
|
||||
>>> Chord.from_tones("Bb", "D", "F").identify()
|
||||
'Bb major'
|
||||
|
||||
# Analyze in a key
|
||||
Chord.from_name("G7").analyze("C") # 'V7'
|
||||
>>> Chord.from_name("G7").analyze("C")
|
||||
'V7'
|
||||
|
||||
Guitar Fingerings
|
||||
-----------------
|
||||
@@ -124,7 +131,7 @@ Guitar Fingerings
|
||||
>>> fb = Fretboard.guitar()
|
||||
|
||||
>>> fb.chord("C")
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=0)
|
||||
Fingering(e=0, B=1, G=0, D=2, A=3, E=x)
|
||||
|
||||
>>> fb.chord("C")['A']
|
||||
3
|
||||
@@ -132,31 +139,38 @@ Guitar Fingerings
|
||||
>>> fb.fingering(0, 0, 0, 2, 2, 0).identify()
|
||||
'E minor'
|
||||
|
||||
>>> from pytheory import CHARTS
|
||||
>>> print(CHARTS["western"]["Am"].tab(fretboard=fb))
|
||||
Am
|
||||
E|--0--
|
||||
>>> print(fb.tab("Am"))
|
||||
A minor
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--2--
|
||||
D|--2--
|
||||
A|--0--
|
||||
E|--0--
|
||||
E|--x--
|
||||
|
||||
>>> from pytheory import Scale
|
||||
>>> pentatonic = Scale(tonic="A4", system="blues")["minor pentatonic"]
|
||||
>>> print(fb.scale_diagram(pentatonic, frets=5))
|
||||
0 1 2 3 4 5
|
||||
E| E | - | - | G | - | A |
|
||||
B| - | C | - | D | - | E |
|
||||
G| G | - | A | - | - | C |
|
||||
D| D | - | E | - | - | G |
|
||||
A| A | - | - | C | - | D |
|
||||
E| E | - | - | G | - | A |
|
||||
|
||||
Audio Playback
|
||||
--------------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Tone, Chord, play, save, Synth
|
||||
>>> from pytheory import Tone, Chord, play, save, Synth
|
||||
|
||||
# Play a tone
|
||||
play(Tone.from_string("A4"), t=1_000)
|
||||
>>> play(Tone.from_string("A4"), t=1_000)
|
||||
|
||||
# Play a chord with a different waveform
|
||||
play(Chord.from_name("Am7"), synth=Synth.TRIANGLE, t=2_000)
|
||||
>>> play(Chord.from_name("Am7"), synth=Synth.TRIANGLE, t=2_000)
|
||||
|
||||
# Save to a WAV file
|
||||
save(Chord.from_name("C"), "c_major.wav", t=2_000)
|
||||
>>> save(Chord.from_name("C"), "c_major.wav", t=2_000)
|
||||
|
||||
Command Line
|
||||
------------
|
||||
|
||||
+148
-129
@@ -30,18 +30,15 @@ Building Scales
|
||||
|
||||
Use :class:`~pytheory.scales.TonedScale` to generate scales in any key:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
|
||||
major = c["major"]
|
||||
minor = c["minor"]
|
||||
harmonic_minor = c["harmonic minor"]
|
||||
|
||||
print(major.note_names)
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
>>> from pytheory import TonedScale
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
>>> major = c["major"]
|
||||
>>> minor = c["minor"]
|
||||
>>> harmonic_minor = c["harmonic minor"]
|
||||
>>> major.note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
Major and Minor
|
||||
---------------
|
||||
@@ -55,13 +52,12 @@ notes but starts from the 6th degree:
|
||||
- G major → E minor (both have one sharp: F#)
|
||||
- F major → D minor (both have one flat: Bb)
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
c_major = TonedScale(tonic="C4")["major"]
|
||||
a_minor = TonedScale(tonic="A4")["minor"]
|
||||
|
||||
# Same notes, different starting point
|
||||
set(c_major.note_names) == set(a_minor.note_names) # True
|
||||
>>> c_major = TonedScale(tonic="C4")["major"]
|
||||
>>> a_minor = TonedScale(tonic="A4")["minor"]
|
||||
>>> set(c_major.note_names) == set(a_minor.note_names)
|
||||
True
|
||||
|
||||
The `harmonic minor <https://en.wikipedia.org/wiki/Harmonic_minor_scale>`_ raises the 7th degree of the natural minor,
|
||||
creating an augmented 2nd interval (3 semitones) between the 6th and
|
||||
@@ -79,44 +75,65 @@ The seven `modes <https://en.wikipedia.org/wiki/Mode_(music)>`_ of the major sca
|
||||
pattern, each starting from a different degree. Each mode has a distinct
|
||||
emotional character:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
|
||||
**Ionian** (I) — the major scale itself. Bright, happy, resolved::
|
||||
**Ionian** (I) — the major scale itself. Bright, happy, resolved:
|
||||
|
||||
c["ionian"] # C D E F G A B C
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["ionian"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
`Dorian <https://en.wikipedia.org/wiki/Dorian_mode>`_ (ii) — minor with a raised 6th. Jazzy, soulful (So What,
|
||||
Scarborough Fair)::
|
||||
Scarborough Fair):
|
||||
|
||||
c["dorian"] # C D Eb F G A Bb C
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["dorian"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
`Phrygian <https://en.wikipedia.org/wiki/Phrygian_mode>`_ (iii) — minor with a flat 2nd. Spanish, flamenco, dark
|
||||
(White Rabbit)::
|
||||
(White Rabbit):
|
||||
|
||||
c["phrygian"] # C Db Eb F G Ab Bb C
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["phrygian"].note_names
|
||||
['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
|
||||
|
||||
`Lydian <https://en.wikipedia.org/wiki/Lydian_mode>`_ (IV) — major with a raised 4th. Dreamy, floating, ethereal
|
||||
(The Simpsons theme, Flying by ET)::
|
||||
(The Simpsons theme, Flying by ET):
|
||||
|
||||
c["lydian"] # C D E F# G A B C
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["lydian"].note_names
|
||||
['C', 'D', 'E', 'F#', 'G', 'A', 'B', 'C']
|
||||
|
||||
`Mixolydian <https://en.wikipedia.org/wiki/Mixolydian_mode>`_ (V) — major with a flat 7th. Bluesy, rock, dominant
|
||||
(Norwegian Wood, Sweet Home Alabama)::
|
||||
(Norwegian Wood, Sweet Home Alabama):
|
||||
|
||||
c["mixolydian"] # C D E F G A Bb C
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["mixolydian"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
`Aeolian <https://en.wikipedia.org/wiki/Aeolian_mode>`_ (vi) — the natural minor scale. Sad, dark, introspective
|
||||
(Stairway to Heaven, Losing My Religion)::
|
||||
(Stairway to Heaven, Losing My Religion):
|
||||
|
||||
c["aeolian"] # C D Eb F G Ab Bb C
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["aeolian"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C']
|
||||
|
||||
`Locrian <https://en.wikipedia.org/wiki/Locrian_mode>`_ (vii) — minor with flat 2nd and flat 5th. Unstable,
|
||||
rarely used as a home key (used in metal and jazz over diminished
|
||||
chords)::
|
||||
chords):
|
||||
|
||||
c["locrian"] # C Db Eb F Gb Ab Bb C
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c["locrian"].note_names
|
||||
['C', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C']
|
||||
|
||||
Scale Degrees
|
||||
-------------
|
||||
@@ -137,32 +154,45 @@ Leading Tone VII One semitone below tonic — pulls upward
|
||||
|
||||
Access degrees by index, Roman numeral, or name:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
major = TonedScale(tonic="C4")["major"]
|
||||
|
||||
major[0] # C4 (by index)
|
||||
major["I"] # C4 (by Roman numeral)
|
||||
major["tonic"] # C4 (by degree name)
|
||||
|
||||
major["V"] # G4 (dominant)
|
||||
major["dominant"] # G4
|
||||
|
||||
major[0:3] # (C4, D4, E4) — slicing works too
|
||||
>>> major = TonedScale(tonic="C4")["major"]
|
||||
>>> major[0]
|
||||
C4
|
||||
>>> major["I"]
|
||||
C4
|
||||
>>> major["tonic"]
|
||||
C4
|
||||
>>> major["V"]
|
||||
G4
|
||||
>>> major["dominant"]
|
||||
G4
|
||||
>>> major[0:3]
|
||||
(<Tone C4>, <Tone D4>, <Tone E4>)
|
||||
|
||||
Iteration
|
||||
---------
|
||||
|
||||
Scales are iterable and support ``len()`` and ``in``:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
for tone in major:
|
||||
print(f"{tone.name}: {tone.frequency:.1f} Hz")
|
||||
|
||||
len(major) # 8 (7 notes + octave)
|
||||
"C" in major # True
|
||||
"C#" in major # False
|
||||
>>> for tone in major:
|
||||
... print(f"{tone.name}: {tone.frequency:.1f} Hz")
|
||||
C: 261.6 Hz
|
||||
D: 293.7 Hz
|
||||
E: 329.6 Hz
|
||||
F: 349.2 Hz
|
||||
G: 392.0 Hz
|
||||
A: 440.0 Hz
|
||||
B: 493.9 Hz
|
||||
C: 523.3 Hz
|
||||
>>> len(major)
|
||||
8
|
||||
>>> "C" in major
|
||||
True
|
||||
>>> "C#" in major
|
||||
False
|
||||
|
||||
Building Chords from Scales
|
||||
----------------------------
|
||||
@@ -185,21 +215,25 @@ Notice the pattern: **major** triads on I, IV, V; **minor** triads on
|
||||
ii, iii, vi; **diminished** on vii°. This pattern holds for every major
|
||||
key.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
major = TonedScale(tonic="C4")["major"]
|
||||
|
||||
# Build diatonic triads
|
||||
I = major.triad(0) # C E G (C major)
|
||||
ii = major.triad(1) # D F A (D minor)
|
||||
iii = major.triad(2) # E G B (E minor)
|
||||
IV = major.triad(3) # F A C (F major)
|
||||
V = major.triad(4) # G B D (G major)
|
||||
vi = major.triad(5) # A C E (A minor)
|
||||
|
||||
# Build seventh chords
|
||||
Imaj7 = major.chord(0, 2, 4, 6) # C E G B = Cmaj7
|
||||
V7 = major.chord(4, 6, 8, 10) # G B D F = G7 (dominant 7th)
|
||||
>>> major = TonedScale(tonic="C4")["major"]
|
||||
>>> major.triad(0)
|
||||
C major
|
||||
>>> major.triad(1)
|
||||
D minor
|
||||
>>> major.triad(2)
|
||||
E minor
|
||||
>>> major.triad(3)
|
||||
F major
|
||||
>>> major.triad(4)
|
||||
G major
|
||||
>>> major.triad(5)
|
||||
A minor
|
||||
>>> major.chord(0, 2, 4, 6)
|
||||
C major 7th
|
||||
>>> major.chord(4, 6, 8, 10)
|
||||
G dominant 7th
|
||||
|
||||
Common Progressions
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -218,31 +252,25 @@ Some of the most-used chord progressions in Western music:
|
||||
The :class:`~pytheory.scales.Key` class makes working with progressions
|
||||
easy:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Key
|
||||
|
||||
key = Key("G", "major")
|
||||
|
||||
# Build a progression from Roman numerals
|
||||
chords = key.progression("I", "V", "vi", "IV")
|
||||
for c in chords:
|
||||
print(c.identify())
|
||||
# G major, D major, E minor, C major
|
||||
|
||||
# Nashville number system (same thing, with integers)
|
||||
key.nashville(1, 5, 6, 4)
|
||||
|
||||
# All diatonic triads in the key
|
||||
key.chords
|
||||
# ['G major', 'A minor', 'B minor', 'C major', ...]
|
||||
|
||||
# All diatonic seventh chords
|
||||
key.seventh_chords
|
||||
# ['G major 7th', 'A minor 7th', ...]
|
||||
|
||||
# Detect the key from a set of notes
|
||||
Key.detect("C", "E", "G", "A", "D") # <Key C major>
|
||||
>>> from pytheory import Key
|
||||
>>> key = Key("G", "major")
|
||||
>>> chords = key.progression("I", "V", "vi", "IV")
|
||||
>>> for c in chords:
|
||||
... print(c.identify())
|
||||
G major
|
||||
D major
|
||||
E minor
|
||||
C major
|
||||
>>> key.nashville(1, 5, 6, 4)
|
||||
[<Chord G major>, <Chord D major>, <Chord E minor>, <Chord C major>]
|
||||
>>> key.chords
|
||||
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
|
||||
>>> key.seventh_chords
|
||||
['G major 7th', 'A minor 7th', 'B minor 7th', 'C major 7th', 'D dominant 7th', 'E minor 7th', 'F# half-diminished 7th']
|
||||
>>> Key.detect("C", "E", "G", "A", "D")
|
||||
C major
|
||||
|
||||
The 12-Bar Blues
|
||||
~~~~~~~~~~~~~~~~
|
||||
@@ -262,31 +290,26 @@ structure. In the key of A::
|
||||
| D | D | A | A |
|
||||
| E | D | A | E |
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
|
||||
a = TonedScale(tonic="A4")["major"]
|
||||
I = a.triad(0) # A major
|
||||
IV = a.triad(3) # D major
|
||||
V = a.triad(4) # E major
|
||||
|
||||
# The 12-bar blues progression
|
||||
blues_12 = [I, I, I, I, IV, IV, I, I, V, IV, I, V]
|
||||
>>> from pytheory import TonedScale
|
||||
>>> a = TonedScale(tonic="A4")["major"]
|
||||
>>> I = a.triad(0)
|
||||
>>> IV = a.triad(3)
|
||||
>>> V = a.triad(4)
|
||||
>>> blues_12 = [I, I, I, I, IV, IV, I, I, V, IV, I, V]
|
||||
|
||||
Key Signatures
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The ``signature`` property tells you how many sharps or flats a key has:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key("G", "major").signature
|
||||
{'sharps': 1, 'flats': 0, 'accidentals': ['F#']}
|
||||
|
||||
>>> Key("F", "major").signature
|
||||
{'sharps': 0, 'flats': 1, 'accidentals': ['Bb']}
|
||||
|
||||
>>> Key("C", "major").signature
|
||||
{'sharps': 0, 'flats': 0, 'accidentals': []}
|
||||
|
||||
@@ -297,16 +320,14 @@ Two keys are **relative** if they share the same notes (C major and
|
||||
A minor). Two keys are `parallel <https://en.wikipedia.org/wiki/Parallel_key>`_ if they share the same tonic but
|
||||
have different notes (C major and C minor):
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key("C", "major").relative
|
||||
<Key A minor>
|
||||
|
||||
A minor
|
||||
>>> Key("A", "minor").relative
|
||||
<Key C major>
|
||||
|
||||
C major
|
||||
>>> Key("C", "major").parallel
|
||||
<Key C minor>
|
||||
C minor
|
||||
|
||||
Borrowed Chords
|
||||
~~~~~~~~~~~~~~~
|
||||
@@ -316,10 +337,10 @@ borrowing chords from the parallel key — is one of the most powerful
|
||||
tools in songwriting. The bVI and bVII chords (Ab and Bb in C major)
|
||||
are borrowed from C minor and appear constantly in rock and film music:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key("C", "major").borrowed_chords
|
||||
# Chords from C minor that aren't in C major
|
||||
['C minor', 'D diminished', 'Eb major', 'F minor', 'G minor', 'Ab major', 'Bb major']
|
||||
|
||||
Secondary Dominants
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -328,15 +349,13 @@ A `secondary dominant <https://en.wikipedia.org/wiki/Secondary_dominant>`_
|
||||
is the V chord *of* a non-tonic chord. It creates a momentary pull
|
||||
toward that chord, adding harmonic color:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
key = Key("C", "major")
|
||||
|
||||
# V/V — the dominant of the dominant (D7 → G)
|
||||
key.secondary_dominant(5) # D dominant 7th
|
||||
|
||||
# V/ii — the dominant of the supertonic (A7 → Dm)
|
||||
key.secondary_dominant(2) # A dominant 7th
|
||||
>>> key = Key("C", "major")
|
||||
>>> key.secondary_dominant(5)
|
||||
D dominant 7th
|
||||
>>> key.secondary_dominant(2)
|
||||
A dominant 7th
|
||||
|
||||
Random Progressions
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -344,19 +363,19 @@ Random Progressions
|
||||
Need inspiration? Generate weighted random progressions. The weights
|
||||
favor common chord functions (I and vi most likely, vii least):
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
key = Key("C", "major")
|
||||
chords = key.random_progression(4) # 4 chords
|
||||
[c.identify() for c in chords]
|
||||
# e.g. ['C major', 'F major', 'A minor', 'G major']
|
||||
>>> key = Key("C", "major")
|
||||
>>> chords = key.random_progression(4)
|
||||
>>> [c.identify() for c in chords]
|
||||
['C major', 'F major', 'A minor', 'G major']
|
||||
|
||||
All Keys
|
||||
~~~~~~~~
|
||||
|
||||
Enumerate all 24 major and minor keys:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key.all_keys()
|
||||
[<Key C major>, <Key C minor>, <Key C# major>, <Key C# minor>, ...]
|
||||
@@ -366,9 +385,9 @@ Scale Transposition
|
||||
|
||||
Transpose an entire scale by a number of semitones:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
c_major = TonedScale(tonic="C4")["major"]
|
||||
d_major = c_major.transpose(2) # Up a whole step
|
||||
d_major.note_names
|
||||
# ['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
|
||||
>>> c_major = TonedScale(tonic="C4")["major"]
|
||||
>>> d_major = c_major.transpose(2)
|
||||
>>> d_major.note_names
|
||||
['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
|
||||
|
||||
+72
-69
@@ -10,16 +10,16 @@ Western
|
||||
The standard 12-tone equal temperament system with major/minor scales
|
||||
and all seven modes.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4")
|
||||
c["major"].note_names
|
||||
# ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
>>> c = TonedScale(tonic="C4")
|
||||
>>> c["major"].note_names
|
||||
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
|
||||
|
||||
c["dorian"].note_names
|
||||
# ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C']
|
||||
>>> c["dorian"].note_names
|
||||
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
**Scales:** major, minor, harmonic minor, ionian, dorian, phrygian,
|
||||
lydian, mixolydian, aeolian, locrian, chromatic
|
||||
@@ -31,20 +31,20 @@ The Hindustani system uses **swaras** (Sa, Re, Ga, Ma, Pa, Dha, Ni) and
|
||||
organizes scales into `thaats <https://en.wikipedia.org/wiki/Thaat>`_ — the 10 parent scales from which `ragas <https://en.wikipedia.org/wiki/Raga>`_
|
||||
are derived.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
sa = TonedScale(tonic="Sa4", system="indian")
|
||||
>>> sa = TonedScale(tonic="Sa4", system="indian")
|
||||
|
||||
sa["bilawal"].note_names # = major scale
|
||||
# ['Sa', 'Re', 'Ga', 'Ma', 'Pa', 'Dha', 'Ni', 'Sa']
|
||||
>>> sa["bilawal"].note_names # = major scale
|
||||
['Sa', 'Re', 'Ga', 'Ma', 'Pa', 'Dha', 'Ni', 'Sa']
|
||||
|
||||
sa["bhairav"].note_names # unique to Indian music
|
||||
# ['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
>>> sa["bhairav"].note_names # unique to Indian music
|
||||
['Sa', 'komal Re', 'Ga', 'Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
sa["todi"].note_names
|
||||
# ['Sa', 'komal Re', 'komal Ga', 'tivra Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
>>> sa["todi"].note_names
|
||||
['Sa', 'komal Re', 'komal Ga', 'tivra Ma', 'Pa', 'komal Dha', 'Ni', 'Sa']
|
||||
|
||||
**Thaats:** bilawal, khamaj, kafi, asavari, bhairavi, kalyan, bhairav,
|
||||
poorvi, marwa, todi
|
||||
@@ -67,20 +67,20 @@ and organizes scales into **maqamat** (plural of `maqam <https://en.wikipedia.or
|
||||
12-tone equal temperament. These scales are the closest 12-TET
|
||||
approximations.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
do = TonedScale(tonic="Do4", system="arabic")
|
||||
>>> do = TonedScale(tonic="Do4", system="arabic")
|
||||
|
||||
do["ajam"].note_names # = major scale
|
||||
# ['Do', 'Re', 'Mi', 'Fa', 'Sol', 'La', 'Si', 'Do']
|
||||
>>> do["ajam"].note_names # = major scale
|
||||
['Do', 'Re', 'Mi', 'Fa', 'Sol', 'La', 'Si', 'Do']
|
||||
|
||||
do["hijaz"].note_names # characteristic augmented 2nd
|
||||
# ['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
|
||||
>>> do["hijaz"].note_names # characteristic augmented 2nd
|
||||
['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do']
|
||||
|
||||
do["nikriz"].note_names
|
||||
# ['Do', 'Re', 'Mib', 'Fa#', 'Sol', 'La', 'Sib', 'Do']
|
||||
>>> do["nikriz"].note_names
|
||||
['Do', 'Re', 'Mib', 'Fa#', 'Sol', 'La', 'Sib', 'Do']
|
||||
|
||||
**Maqamat:** ajam, nahawand, kurd, hijaz, nikriz, bayati, rast, saba,
|
||||
sikah, jiharkah
|
||||
@@ -91,23 +91,23 @@ Japanese
|
||||
The Japanese system uses Western note names with traditional pentatonic
|
||||
and heptatonic scales from Japanese music.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4", system="japanese")
|
||||
>>> c = TonedScale(tonic="C4", system="japanese")
|
||||
|
||||
c["hirajoshi"].note_names # most iconic Japanese scale
|
||||
# ['C', 'D', 'D#', 'G', 'G#', 'C']
|
||||
>>> c["hirajoshi"].note_names # most iconic Japanese scale
|
||||
['C', 'D', 'Eb', 'G', 'Ab', 'C']
|
||||
|
||||
c["in"].note_names # Miyako-bushi, used in koto music
|
||||
# ['C', 'C#', 'F', 'G', 'G#', 'C']
|
||||
>>> c["in"].note_names # Miyako-bushi, used in koto music
|
||||
['C', 'Db', 'F', 'G', 'Ab', 'C']
|
||||
|
||||
c["yo"].note_names # folk music scale
|
||||
# ['C', 'D', 'F', 'G', 'A#', 'C']
|
||||
>>> c["yo"].note_names # folk music scale
|
||||
['C', 'D', 'F', 'G', 'A#', 'C']
|
||||
|
||||
c["ritsu"].note_names # gagaku court music (= Dorian)
|
||||
# ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C']
|
||||
>>> c["ritsu"].note_names # gagaku court music (= Dorian)
|
||||
['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb', 'C']
|
||||
|
||||
**Pentatonic scales:** hirajoshi, in, yo, iwato, kumoi, insen
|
||||
|
||||
@@ -125,23 +125,23 @@ The `blues scale <https://en.wikipedia.org/wiki/Blues_scale>`_ adds the "`blue n
|
||||
minor pentatonic — this chromatic passing tone is the defining sound
|
||||
of the blues.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
c = TonedScale(tonic="C4", system="blues")
|
||||
>>> c = TonedScale(tonic="C4", system="blues")
|
||||
|
||||
c["major pentatonic"].note_names # the "happy" pentatonic
|
||||
# ['C', 'D', 'E', 'G', 'A', 'C']
|
||||
>>> c["major pentatonic"].note_names # the "happy" pentatonic
|
||||
['C', 'D', 'E', 'G', 'A', 'C']
|
||||
|
||||
c["minor pentatonic"].note_names # the "sad" pentatonic
|
||||
# ['C', 'D#', 'F', 'G', 'A#', 'C']
|
||||
>>> c["minor pentatonic"].note_names # the "sad" pentatonic
|
||||
['C', 'D#', 'F', 'G', 'A#', 'C']
|
||||
|
||||
c["blues"].note_names # minor pentatonic + blue note
|
||||
# ['C', 'D#', 'F', 'F#', 'G', 'A#', 'C']
|
||||
>>> c["blues"].note_names # minor pentatonic + blue note
|
||||
['C', 'Eb', 'F', 'Gb', 'G', 'Bb', 'C']
|
||||
|
||||
c["major blues"].note_names # major pentatonic + blue note
|
||||
# ['C', 'D', 'D#', 'E', 'G', 'A', 'C']
|
||||
>>> c["major blues"].note_names # major pentatonic + blue note
|
||||
['C', 'D', 'Eb', 'E', 'G', 'A', 'C']
|
||||
|
||||
**Pentatonic:** major pentatonic, minor pentatonic
|
||||
|
||||
@@ -163,20 +163,20 @@ these are the closest 12-TET approximations.
|
||||
an ethereal, floating quality. `Pelog <https://en.wikipedia.org/wiki/Pelog>`_ is a 7-tone scale with unequal
|
||||
intervals, typically performed using 5-note subsets called *pathet*.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
ji = TonedScale(tonic="ji4", system="gamelan")
|
||||
>>> ji = TonedScale(tonic="ji4", system="gamelan")
|
||||
|
||||
ji["slendro"].note_names # the 5-tone equidistant scale
|
||||
# ['ji', 'ro', 'pat', 'mo', 'pi', 'ji']
|
||||
>>> ji["slendro"].note_names # the 5-tone equidistant scale
|
||||
['ji', 'ro', 'pat', 'mo', 'pi', 'ji']
|
||||
|
||||
ji["pelog"].note_names # full 7-tone pelog
|
||||
# ['ji', 'ro-', 'lu', 'pat', 'mo', 'nem-', 'barang', 'ji']
|
||||
>>> ji["pelog"].note_names # full 7-tone pelog
|
||||
['ji', 'ro-', 'lu', 'pat', 'mo', 'nem-', 'barang', 'ji']
|
||||
|
||||
ji["pelog nem"].note_names # pathet nem subset
|
||||
# ['ji', 'ro-', 'lu', 'pat', 'mo', 'ji']
|
||||
>>> ji["pelog nem"].note_names # pathet nem subset
|
||||
['ji', 'ro-', 'lu', 'pat', 'mo', 'ji']
|
||||
|
||||
**Pentatonic:** slendro, pelog nem, pelog barang, pelog lima
|
||||
|
||||
@@ -195,20 +195,23 @@ Cross-System Comparison
|
||||
Since all systems use 12-tone equal temperament, equivalent scales
|
||||
produce the same pitches:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale, Tone
|
||||
>>> from pytheory import TonedScale, Tone
|
||||
|
||||
# These are all the same scale with different names
|
||||
western = TonedScale(tonic="C4")["major"]
|
||||
indian = TonedScale(tonic="Sa4", system="indian")["bilawal"]
|
||||
arabic = TonedScale(tonic="Do4", system="arabic")["ajam"]
|
||||
>>> # These are all the same scale with different names
|
||||
>>> western = TonedScale(tonic="C4")["major"]
|
||||
>>> indian = TonedScale(tonic="Sa4", system="indian")["bilawal"]
|
||||
>>> arabic = TonedScale(tonic="Do4", system="arabic")["ajam"]
|
||||
|
||||
# Same pitches
|
||||
c4 = Tone.from_string("C4", system="western")
|
||||
sa4 = Tone.from_string("Sa4", system="indian")
|
||||
do4 = Tone.from_string("Do4", system="arabic")
|
||||
>>> # Same pitches
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> sa4 = Tone.from_string("Sa4", system="indian")
|
||||
>>> do4 = Tone.from_string("Do4", system="arabic")
|
||||
|
||||
c4.frequency # 261.63
|
||||
sa4.frequency # 261.63
|
||||
do4.frequency # 261.63
|
||||
>>> c4.frequency
|
||||
261.6255653005986
|
||||
>>> sa4.frequency
|
||||
261.6255653005986
|
||||
>>> do4.frequency
|
||||
261.6255653005986
|
||||
|
||||
+53
-54
@@ -50,14 +50,13 @@ cycle almost closes. The tiny gap where it doesn't close perfectly is
|
||||
the `Pythagorean comma <https://en.wikipedia.org/wiki/Pythagorean_comma>`_
|
||||
— the reason we need `temperament <https://en.wikipedia.org/wiki/Musical_temperament>`_.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Tone
|
||||
>>> from pytheory import Tone
|
||||
|
||||
# Walk the circle of fifths — all 12 notes
|
||||
c = Tone.from_string("C4", system="western")
|
||||
[t.name for t in c.circle_of_fifths()]
|
||||
# ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
|
||||
>>> c = Tone.from_string("C4", system="western")
|
||||
>>> [t.name for t in c.circle_of_fifths()]
|
||||
['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
|
||||
|
||||
Other cultures divide the octave differently: Indonesian
|
||||
`gamelan <https://en.wikipedia.org/wiki/Gamelan>`_ uses 5 or 7 unequal
|
||||
@@ -183,17 +182,18 @@ is exactly this pattern. Every "Louie Louie" and every
|
||||
`Bach chorale <https://en.wikipedia.org/wiki/Bach_chorale>`_ follows
|
||||
this basic tonal gravity.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import TonedScale
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
scale = TonedScale(tonic="C4")["major"]
|
||||
>>> scale = TonedScale(tonic="C4")["major"]
|
||||
|
||||
# The I-IV-V-I progression
|
||||
I = scale.triad(0) # C major — home
|
||||
IV = scale.triad(3) # F major — departure
|
||||
V = scale.triad(4) # G major — tension
|
||||
# I again # C major — resolution
|
||||
>>> scale.triad(0).identify()
|
||||
'C major'
|
||||
>>> scale.triad(3).identify()
|
||||
'F major'
|
||||
>>> scale.triad(4).identify()
|
||||
'G major'
|
||||
|
||||
The Dominant Seventh
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -210,20 +210,24 @@ This combination creates the strongest possible pull toward
|
||||
`resolution <https://en.wikipedia.org/wiki/Resolution_(music)>`_.
|
||||
When you hear V7→I, you feel arrival.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
C4 = Tone.from_string("C4", system="western")
|
||||
G4 = Tone.from_string("G4", system="western")
|
||||
>>> C4 = Tone.from_string("C4", system="western")
|
||||
>>> G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
g7 = Chord([G4, G4+4, G4+7, G4+10]) # G B D F
|
||||
g7.identify() # 'G dominant 7th'
|
||||
g7.tension['has_dominant_function'] # True
|
||||
g7.tension['tritones'] # 1
|
||||
>>> g7 = Chord([G4, G4+4, G4+7, G4+10])
|
||||
>>> g7.identify()
|
||||
'G dominant 7th'
|
||||
>>> g7.tension['has_dominant_function']
|
||||
True
|
||||
>>> g7.tension['tritones']
|
||||
1
|
||||
|
||||
c_major = Chord([C4, C4+4, C4+7]) # C E G
|
||||
c_major.tension['score'] # 0.0 — fully resolved
|
||||
>>> c_major = Chord([C4, C4+4, C4+7])
|
||||
>>> c_major.tension['score']
|
||||
0.0
|
||||
|
||||
Rhythm and Meter
|
||||
----------------
|
||||
@@ -277,43 +281,38 @@ foundation of blues and jazz. Indonesian gamelan embraces
|
||||
`beating <https://en.wikipedia.org/wiki/Beat_(acoustics)>`_ between
|
||||
paired instruments as a core aesthetic.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Chord, Tone
|
||||
>>> from pytheory import Chord, Tone
|
||||
|
||||
C4 = Tone.from_string("C4", system="western")
|
||||
E4 = Tone.from_string("E4", system="western")
|
||||
G4 = Tone.from_string("G4", system="western")
|
||||
>>> C4 = Tone.from_string("C4", system="western")
|
||||
>>> E4 = Tone.from_string("E4", system="western")
|
||||
>>> G4 = Tone.from_string("G4", system="western")
|
||||
|
||||
# The overtone series — the fifth is "built into" every tone
|
||||
C4.overtones(6)
|
||||
# [261.63, 523.25, 784.88, 1046.50, 1308.13, 1569.75]
|
||||
# 3rd harmonic (784.88) ≈ G5 (783.99) — a perfect fifth
|
||||
>>> [round(f, 2) for f in C4.overtones(6)]
|
||||
[261.63, 523.25, 784.88, 1046.5, 1308.13, 1569.75]
|
||||
|
||||
# Consonance: simple frequency ratios score high
|
||||
fifth = Chord([C4, G4]) # 3:2 ratio
|
||||
tritone = Chord([C4, C4 + 6]) # 45:32 ratio
|
||||
fifth.harmony > tritone.harmony # True
|
||||
>>> fifth = Chord([C4, G4])
|
||||
>>> tritone = Chord([C4, C4 + 6])
|
||||
>>> fifth.harmony > tritone.harmony
|
||||
True
|
||||
|
||||
# Dissonance: Plomp-Levelt roughness model
|
||||
# An octave has low roughness (frequencies far apart)
|
||||
# A major 3rd has more roughness (closer frequencies)
|
||||
octave = Chord([C4, C4 + 12])
|
||||
third = Chord([C4, E4])
|
||||
octave.dissonance < third.dissonance # True
|
||||
>>> octave = Chord([C4, C4 + 12])
|
||||
>>> third = Chord([C4, E4])
|
||||
>>> octave.dissonance < third.dissonance
|
||||
True
|
||||
|
||||
# Tension: tritones and dominant function
|
||||
c_major = Chord([C4, E4, G4])
|
||||
c_major.tension['score'] # 0.0 — fully resolved
|
||||
>>> c_major = Chord([C4, E4, G4])
|
||||
>>> c_major.tension['score']
|
||||
0.0
|
||||
|
||||
g7 = Chord([G4, G4+4, G4+7, G4+10]) # G dominant 7th
|
||||
g7.tension['score'] # 0.6 — wants to resolve
|
||||
g7.tension['tritones'] # 1 (B-F)
|
||||
g7.tension['has_dominant_function'] # True
|
||||
|
||||
# Beat frequencies — the pulsing between close pitches
|
||||
g7.beat_frequencies
|
||||
# [(tone_a, tone_b, hz), ...] sorted by frequency
|
||||
>>> g7 = Chord([G4, G4+4, G4+7, G4+10])
|
||||
>>> g7.tension['score']
|
||||
0.6
|
||||
>>> g7.tension['tritones']
|
||||
1
|
||||
>>> g7.tension['has_dominant_function']
|
||||
True
|
||||
|
||||
Further Reading
|
||||
---------------
|
||||
|
||||
+53
-63
@@ -40,33 +40,32 @@ Key reference points:
|
||||
Creating Tones
|
||||
--------------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
from pytheory import Tone
|
||||
>>> from pytheory import Tone
|
||||
|
||||
# From a string (most common) — sharps and flats both work
|
||||
c4 = Tone.from_string("C4")
|
||||
cs4 = Tone.from_string("C#4")
|
||||
db4 = Tone.from_string("Db4") # Same pitch as C#4
|
||||
>>> c4 = Tone.from_string("C4")
|
||||
>>> cs4 = Tone.from_string("C#4")
|
||||
>>> db4 = Tone.from_string("Db4")
|
||||
|
||||
# Direct construction
|
||||
d = Tone(name="D", octave=3)
|
||||
>>> d = Tone(name="D", octave=3)
|
||||
|
||||
# With a specific system
|
||||
a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
|
||||
# From a frequency (finds the nearest note)
|
||||
Tone.from_frequency(440) # <Tone A4>
|
||||
Tone.from_frequency(261.63) # <Tone C4>
|
||||
>>> Tone.from_frequency(440)
|
||||
<Tone A4>
|
||||
>>> Tone.from_frequency(261.63)
|
||||
<Tone C4>
|
||||
|
||||
# From a MIDI note number
|
||||
Tone.from_midi(60) # <Tone C4> (middle C)
|
||||
Tone.from_midi(69) # <Tone A4>
|
||||
>>> Tone.from_midi(60)
|
||||
<Tone C4>
|
||||
>>> Tone.from_midi(69)
|
||||
<Tone A4>
|
||||
|
||||
Properties
|
||||
----------
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> c4.name
|
||||
@@ -75,11 +74,11 @@ Properties
|
||||
4
|
||||
>>> c4.full_name
|
||||
'C4'
|
||||
>>> c4.letter # Note letter without accidentals
|
||||
>>> c4.letter
|
||||
'C'
|
||||
>>> c4.midi # MIDI note number
|
||||
>>> c4.midi
|
||||
60
|
||||
>>> c4.exists # Is this note in the system?
|
||||
>>> c4.exists
|
||||
True
|
||||
|
||||
Pitch and Frequency
|
||||
@@ -90,17 +89,17 @@ cycles per second). The relationship between pitch and frequency is
|
||||
**logarithmic**: each octave doubles the frequency, and each semitone
|
||||
multiplies by the 12th root of 2 (~1.05946).
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.frequency
|
||||
440.0
|
||||
|
||||
>>> Tone.from_string("A3", system="western").frequency
|
||||
220.0 # One octave down = half the frequency
|
||||
220.0
|
||||
|
||||
>>> Tone.from_string("C4", system="western").frequency
|
||||
261.63 # Middle C
|
||||
261.6255653005986
|
||||
|
||||
Temperament
|
||||
~~~~~~~~~~~
|
||||
@@ -125,18 +124,18 @@ same note name:
|
||||
in closely related keys but "wolf intervals" make distant keys
|
||||
unusable.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a4.pitch(temperament="equal")
|
||||
440.0
|
||||
>>> a4.pitch(temperament="pythagorean")
|
||||
440.0 # A4 is always 440 (it's the reference)
|
||||
440.0
|
||||
|
||||
>>> c5 = Tone.from_string("C5", system="western")
|
||||
>>> c5.pitch(temperament="equal")
|
||||
523.25
|
||||
523.2511306011972
|
||||
>>> c5.pitch(temperament="pythagorean")
|
||||
521.48 # Slightly different!
|
||||
521.4814814814815
|
||||
|
||||
Symbolic Pitch
|
||||
~~~~~~~~~~~~~~
|
||||
@@ -147,33 +146,29 @@ floating-point approximations. This is useful for mathematical analysis,
|
||||
proving tuning relationships, or comparing temperaments with exact
|
||||
arithmetic.
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
|
||||
# Equal temperament: irrational ratios (roots of 2)
|
||||
>>> a4.pitch(symbolic=True)
|
||||
440
|
||||
>>> Tone.from_string("C5", system="western").pitch(symbolic=True)
|
||||
440*2**(1/4)
|
||||
|
||||
# Pythagorean: pure rational ratios (powers of 3/2)
|
||||
>>> Tone.from_string("G4", system="western").pitch(
|
||||
... temperament="pythagorean", symbolic=True)
|
||||
660
|
||||
391.111111111111
|
||||
|
||||
# Compare the major third across temperaments
|
||||
>>> e4 = Tone.from_string("E4", system="western")
|
||||
>>> e4.pitch(temperament="equal", symbolic=True)
|
||||
440*2**(1/3)
|
||||
220.0*2**(7/12)
|
||||
>>> e4.pitch(temperament="pythagorean", symbolic=True)
|
||||
12160/27
|
||||
330.000000000000
|
||||
>>> e4.pitch(temperament="meantone", symbolic=True)
|
||||
550
|
||||
220.0*5**(1/4)
|
||||
|
||||
# Symbolic expressions can be evaluated to any precision
|
||||
>>> e4.pitch(symbolic=True).evalf(50)
|
||||
329.62755691286991583007431157433859631791591649985
|
||||
329.62755691286992973584176104655507518647334182098
|
||||
|
||||
The symbolic output reveals *why* temperaments differ: equal temperament
|
||||
uses irrational numbers (roots of 2), Pythagorean uses powers of 3/2
|
||||
@@ -207,26 +202,26 @@ Common intervals::
|
||||
|
||||
Tones support ``+`` and ``-`` operators for semitone math:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
>>> c4 + 4 # Major third up
|
||||
>>> c4 + 4
|
||||
<Tone E4>
|
||||
>>> c4 + 7 # Perfect fifth up
|
||||
>>> c4 + 7
|
||||
<Tone G4>
|
||||
>>> c4 + 12 # Octave up
|
||||
>>> c4 + 12
|
||||
<Tone C5>
|
||||
|
||||
Subtracting two tones gives the semitone distance:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> g4 = Tone.from_string("G4", system="western")
|
||||
>>> g4 - c4 # Perfect fifth = 7 semitones
|
||||
>>> g4 - c4
|
||||
7
|
||||
|
||||
>>> c5 = Tone.from_string("C5", system="western")
|
||||
>>> c5 - c4 # Octave = 12 semitones
|
||||
>>> c5 - c4
|
||||
12
|
||||
|
||||
Naming Intervals
|
||||
@@ -236,7 +231,7 @@ The ``interval_to`` method gives the musical name of the interval
|
||||
between two tones, including compound intervals that span more than
|
||||
one octave:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4.interval_to(g4)
|
||||
'perfect 5th'
|
||||
@@ -245,8 +240,7 @@ one octave:
|
||||
>>> c4.interval_to(c5)
|
||||
'octave'
|
||||
|
||||
# Compound intervals (more than an octave)
|
||||
>>> c4.interval_to(c4 + 19) # Octave + perfect 5th
|
||||
>>> c4.interval_to(c4 + 19)
|
||||
'perfect 5th + 1 octave'
|
||||
|
||||
Transposition
|
||||
@@ -256,11 +250,11 @@ The ``transpose`` method returns a new tone shifted by a number of
|
||||
semitones — equivalent to the ``+`` operator but reads more clearly
|
||||
in some contexts:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4.transpose(7) # Same as c4 + 7
|
||||
>>> c4.transpose(7)
|
||||
<Tone G4>
|
||||
>>> c4.transpose(-2) # Two semitones down
|
||||
>>> c4.transpose(-2)
|
||||
<Tone A#3>
|
||||
|
||||
MIDI
|
||||
@@ -270,14 +264,13 @@ Every tone maps to a `MIDI note number <https://en.wikipedia.org/wiki/MIDI>`_
|
||||
(0–127), the standard for communicating with synthesizers, DAWs, and
|
||||
digital instruments:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4.midi
|
||||
60 # Middle C
|
||||
60
|
||||
>>> Tone.from_string("A4", system="western").midi
|
||||
69 # Concert A
|
||||
69
|
||||
|
||||
# Round-trip: MIDI → Tone → MIDI
|
||||
>>> Tone.from_midi(60).midi
|
||||
60
|
||||
|
||||
@@ -286,7 +279,7 @@ Comparison and Sorting
|
||||
|
||||
Tones can be compared and sorted by pitch frequency:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 < g4
|
||||
True
|
||||
@@ -295,9 +288,9 @@ Tones can be compared and sorted by pitch frequency:
|
||||
|
||||
Equality checks note name and octave:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 == "C" # Compare with string (name only)
|
||||
>>> c4 == "C"
|
||||
True
|
||||
>>> c4 == Tone(name="C", octave=4)
|
||||
True
|
||||
@@ -309,7 +302,7 @@ Every tone you hear is actually a composite of many frequencies. When
|
||||
a string vibrates, it doesn't just vibrate as a whole — it also vibrates
|
||||
in halves, thirds, quarters, and so on, producing the `harmonic series <https://en.wikipedia.org/wiki/Harmonic_series_(music)>`_:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a4 = Tone.from_string("A4", system="western")
|
||||
>>> a4.overtones(8)
|
||||
@@ -353,7 +346,7 @@ F#=Gb and C#=Db.
|
||||
PyTheory uses sharps by default (following the tone list ordering), but
|
||||
every tone knows its enharmonic spelling:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("C#4", system="western").enharmonic
|
||||
'Db'
|
||||
@@ -361,7 +354,6 @@ every tone knows its enharmonic spelling:
|
||||
>>> Tone.from_string("A#4", system="western").enharmonic
|
||||
'Bb'
|
||||
|
||||
# Natural notes have no enharmonic
|
||||
>>> Tone.from_string("C4", system="western").enharmonic is None
|
||||
True
|
||||
|
||||
@@ -373,15 +365,13 @@ theory. Starting from any note and ascending by perfect fifths (7
|
||||
semitones), you pass through all 12 chromatic tones before returning
|
||||
to the starting note:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c4 = Tone.from_string("C4", system="western")
|
||||
|
||||
# Clockwise — ascending fifths (adds sharps)
|
||||
>>> [t.name for t in c4.circle_of_fifths()]
|
||||
['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
|
||||
|
||||
# Counter-clockwise — ascending fourths (adds flats)
|
||||
>>> [t.name for t in c4.circle_of_fourths()]
|
||||
['C', 'F', 'A#', 'D#', 'G#', 'C#', 'F#', 'B', 'E', 'A', 'D', 'G']
|
||||
|
||||
|
||||
+27
-16
@@ -11,7 +11,7 @@ instruments using a clean, Pythonic API.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key, Chord, Tone, Fretboard
|
||||
>>> from pytheory import Key, Chord, Tone, Scale, Fretboard
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> key.chords
|
||||
@@ -32,6 +32,31 @@ instruments using a clean, Pythonic API.
|
||||
>>> fb.chord("G")
|
||||
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
|
||||
|
||||
>>> pentatonic = Scale(tonic="A4", system="blues")["minor pentatonic"]
|
||||
>>> print(fb.scale_diagram(pentatonic, frets=5))
|
||||
0 1 2 3 4 5
|
||||
E| E | - | - | G | - | A |
|
||||
B| - | C | - | D | - | E |
|
||||
G| G | - | A | - | - | C |
|
||||
D| D | - | E | - | - | G |
|
||||
A| A | - | - | C | - | D |
|
||||
E| E | - | - | G | - | A |
|
||||
|
||||
Highlights
|
||||
----------
|
||||
|
||||
- **Tones**: frequencies, MIDI, intervals, transposition, circle of fifths,
|
||||
overtone series, 3 temperaments (equal, Pythagorean, meantone)
|
||||
- **Scales**: 40+ scales across 6 musical systems — Western, Indian,
|
||||
Arabic, Japanese, Blues, Javanese Gamelan
|
||||
- **Chords**: 17 chord types identified automatically, Roman numeral
|
||||
analysis, tension scoring, voice leading, consonance/dissonance
|
||||
- **Keys**: key detection, signatures, progressions (Roman numerals and
|
||||
Nashville numbers), borrowed chords, secondary dominants
|
||||
- **Instruments**: 25 presets (guitar, bass, ukulele, mandolin, violin,
|
||||
banjo, oud, sitar, erhu, and more) with fingering generation
|
||||
- **Audio**: sine, sawtooth, and triangle wave playback + WAV export
|
||||
|
||||
It also works from the command line::
|
||||
|
||||
$ pytheory key G major
|
||||
@@ -50,21 +75,6 @@ It also works from the command line::
|
||||
Playing: A minor 7th (A4 C4 E4 G4)
|
||||
Synth: triangle
|
||||
|
||||
Highlights
|
||||
----------
|
||||
|
||||
- **Tones**: frequencies, MIDI, intervals, transposition, circle of fifths,
|
||||
overtone series, 3 temperaments (equal, Pythagorean, meantone)
|
||||
- **Scales**: 40+ scales across 6 musical systems — Western, Indian,
|
||||
Arabic, Japanese, Blues, Javanese Gamelan
|
||||
- **Chords**: 17 chord types identified automatically, Roman numeral
|
||||
analysis, tension scoring, voice leading, consonance/dissonance
|
||||
- **Keys**: key detection, signatures, progressions (Roman numerals and
|
||||
Nashville numbers), borrowed chords, secondary dominants
|
||||
- **Instruments**: 25 presets (guitar, bass, ukulele, mandolin, violin,
|
||||
banjo, oud, sitar, erhu, and more) with fingering generation
|
||||
- **Audio**: sine, sawtooth, and triangle wave playback + WAV export
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: User Guide
|
||||
@@ -78,6 +88,7 @@ Highlights
|
||||
guide/systems
|
||||
guide/playback
|
||||
guide/cli
|
||||
guide/cookbook
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.7.0"
|
||||
version = "0.8.2"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -44,5 +44,8 @@ docs = ["sphinx"]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["pytheory"]
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.7.0"
|
||||
__version__ = "0.8.2"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS
|
||||
from .scales import Scale, TonedScale, Key, PROGRESSIONS
|
||||
from .scales import TonedScale, Key, PROGRESSIONS
|
||||
from .chords import Chord, Fretboard, analyze_progression
|
||||
from .charts import CHARTS, Fingering, charts_for_fretboard
|
||||
|
||||
try:
|
||||
from .play import play, save, Synth
|
||||
from .play import play, save, play_progression, Synth
|
||||
except OSError:
|
||||
play = None
|
||||
save = None
|
||||
play_progression = None
|
||||
Synth = None
|
||||
|
||||
# Aliases for discoverability.
|
||||
Note = Tone
|
||||
Scale = TonedScale
|
||||
|
||||
__all__ = [
|
||||
"Tone", "Note", "Interval", "Scale", "TonedScale", "Key",
|
||||
"PROGRESSIONS", "Chord", "Fretboard", "Fingering", "analyze_progression",
|
||||
"System", "SYSTEMS", "CHARTS", "charts_for_fretboard",
|
||||
"play", "save", "Synth",
|
||||
"play", "save", "play_progression", "Synth",
|
||||
]
|
||||
|
||||
+264
-36
@@ -1,3 +1,4 @@
|
||||
import functools
|
||||
import itertools
|
||||
from typing import Optional
|
||||
|
||||
@@ -7,6 +8,39 @@ from .tones import Tone
|
||||
QUALITIES = ("", "maj", "m", "5", "7", "9", "dim", "m6", "m7", "m9", "maj7", "maj9")
|
||||
MAX_FRET = 7
|
||||
|
||||
# Standard guitar tuning (high to low): E4 B3 G3 D3 A2 E2
|
||||
STANDARD_GUITAR_TUNING = ("E4", "B3", "G3", "D3", "A2", "E2")
|
||||
|
||||
# Curated override fingerings for common guitar chords in standard tuning.
|
||||
# Key: chord name, Value: tuple of fret positions (-1 = muted string).
|
||||
# Order is high-to-low (matching Fretboard.guitar() string order).
|
||||
GUITAR_OVERRIDES = {
|
||||
"C": (0, 1, 0, 2, 3, -1),
|
||||
"D": (2, 3, 2, 0, -1, -1),
|
||||
"Dm": (1, 3, 2, 0, -1, -1),
|
||||
"D7": (2, 1, 2, 0, -1, -1),
|
||||
"E": (0, 0, 1, 2, 2, 0),
|
||||
"Em": (0, 0, 0, 2, 2, 0),
|
||||
"F": (1, 1, 2, 3, 3, 1),
|
||||
"G": (3, 0, 0, 0, 2, 3),
|
||||
"G7": (1, 0, 0, 0, 2, 3),
|
||||
"A": (0, 2, 2, 2, 0, -1),
|
||||
"Am": (0, 1, 2, 2, 0, -1),
|
||||
"Am7": (0, 1, 0, 2, 0, -1),
|
||||
"B": (2, 4, 4, 4, 2, -1),
|
||||
"Bm": (2, 3, 4, 4, 2, -1),
|
||||
"B7": (2, 0, 2, 1, 2, -1),
|
||||
}
|
||||
|
||||
# Memoization cache for fingering lookups.
|
||||
# Key: (chord_name, fretboard_tuning_tuple)
|
||||
# Value: Fingering object (single) or tuple of Fingerings (multiple)
|
||||
# Bounded to _CACHE_MAX_SIZE entries; cleared entirely when full.
|
||||
_CACHE_MAX_SIZE = 1024
|
||||
_fingering_cache: dict[tuple, "Fingering"] = {}
|
||||
_fingering_multi_cache: dict[tuple, tuple] = {}
|
||||
_possible_cache: dict[tuple, tuple] = {}
|
||||
|
||||
|
||||
class Fingering:
|
||||
"""A chord fingering labeled with string names.
|
||||
@@ -107,6 +141,33 @@ class Fingering:
|
||||
"""Identify the chord name from this fingering."""
|
||||
return self.to_chord().identify()
|
||||
|
||||
def tab(self) -> str:
|
||||
"""Render this fingering as ASCII guitar tablature.
|
||||
|
||||
Requires that the Fingering was created with a fretboard reference.
|
||||
|
||||
Example::
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> print(fb.chord("C").tab())
|
||||
C
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
A|--3--
|
||||
E|--0--
|
||||
"""
|
||||
if self._fretboard is None:
|
||||
raise ValueError("Cannot render tab without a fretboard reference.")
|
||||
name = self.identify() or "?"
|
||||
lines = [name]
|
||||
max_name = max(len(n) for n in self.string_names)
|
||||
for sname, fret in zip(self.string_names, self.positions):
|
||||
fret_str = "x" if fret is None else str(fret)
|
||||
lines.append(f"{sname:>{max_name}}|--{fret_str}--")
|
||||
return "\n".join(lines)
|
||||
|
||||
CHARTS = {}
|
||||
CHARTS["western"] = []
|
||||
|
||||
@@ -132,65 +193,108 @@ class NamedChord:
|
||||
def __repr__(self):
|
||||
return f"<NamedChord name={self.name!r}>"
|
||||
|
||||
@property
|
||||
def _prefer_flats(self):
|
||||
"""Determine whether this chord's tones should use flat spellings.
|
||||
|
||||
Uses the circle-of-fifths convention:
|
||||
- Flat-root notes (Bb, Eb, Ab, Db, Gb) always prefer flats.
|
||||
- Major-type qualities prefer flats for roots: F, Bb, Eb, Ab, Db, Gb.
|
||||
- Minor-type qualities prefer flats for roots: D, G, C, F, Bb, Eb, Ab.
|
||||
"""
|
||||
# Root is itself a flat note — always prefer flats
|
||||
if "b" in self.tone_name and self.tone_name != "B":
|
||||
return True
|
||||
|
||||
_FLAT_MAJOR_ROOTS = {"F", "Bb", "Eb", "Ab", "Db", "Gb"}
|
||||
_FLAT_MINOR_ROOTS = {"D", "G", "C", "F", "Bb", "Eb", "Ab"}
|
||||
# Dominant 7th/9th chords contain a minor 7th (b7), so they
|
||||
# follow the same flat-preference roots as minor chords.
|
||||
_FLAT_DOMINANT_ROOTS = {"C", "F", "G", "Bb", "Eb", "Ab", "Db", "Gb"}
|
||||
|
||||
minor_qualities = {"m", "m6", "m7", "m9", "dim"}
|
||||
dominant_qualities = {"7", "9"}
|
||||
major_qualities = {"", "maj", "5", "maj7", "maj9"}
|
||||
|
||||
if self.quality in minor_qualities and self.tone_name in _FLAT_MINOR_ROOTS:
|
||||
return True
|
||||
if self.quality in dominant_qualities and self.tone_name in _FLAT_DOMINANT_ROOTS:
|
||||
return True
|
||||
if self.quality in major_qualities and self.tone_name in _FLAT_MAJOR_ROOTS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def acceptable_tones(self):
|
||||
acceptable = [self.tone]
|
||||
flats = self._prefer_flats
|
||||
|
||||
if self.quality == "maj":
|
||||
# Major triad: root, major 3rd, perfect 5th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "m":
|
||||
# Minor triad: root, minor 3rd, perfect 5th
|
||||
acceptable += [self.tone.add(3), self.tone.add(7)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "5":
|
||||
# Power chord: root, perfect 5th
|
||||
acceptable += [self.tone.add(7)]
|
||||
acceptable += [self.tone.add(7, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "7":
|
||||
# Dominant 7th: root, major 3rd, perfect 5th, minor 7th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(10)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "9":
|
||||
# Dominant 9th: root, major 3rd, perfect 5th, minor 7th, major 9th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(10), self.tone.add(2)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats), self.tone.add(2, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "dim":
|
||||
# Diminished: root, minor 3rd, diminished 5th
|
||||
acceptable += [self.tone.add(3), self.tone.add(6)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(6, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "m6":
|
||||
# Minor 6th: root, minor 3rd, perfect 5th, major 6th
|
||||
acceptable += [self.tone.add(3), self.tone.add(7), self.tone.add(9)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(9, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "m7":
|
||||
# Minor 7th: root, minor 3rd, perfect 5th, minor 7th
|
||||
acceptable += [self.tone.add(3), self.tone.add(7), self.tone.add(10)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "m9":
|
||||
# Minor 9th: root, minor 3rd, perfect 5th, minor 7th, major 9th
|
||||
acceptable += [self.tone.add(3), self.tone.add(7), self.tone.add(10), self.tone.add(2)]
|
||||
acceptable += [self.tone.add(3, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(10, prefer_flats=flats), self.tone.add(2, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "maj7":
|
||||
# Major 7th: root, major 3rd, perfect 5th, major 7th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(11)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(11, prefer_flats=flats)]
|
||||
|
||||
elif self.quality == "maj9":
|
||||
# Major 9th: root, major 3rd, perfect 5th, major 7th, major 9th
|
||||
acceptable += [self.tone.add(4), self.tone.add(7), self.tone.add(11), self.tone.add(2)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats), self.tone.add(11, prefer_flats=flats), self.tone.add(2, prefer_flats=flats)]
|
||||
|
||||
else:
|
||||
# Default (no quality): major triad
|
||||
acceptable += [self.tone.add(4), self.tone.add(7)]
|
||||
acceptable += [self.tone.add(4, prefer_flats=flats), self.tone.add(7, prefer_flats=flats)]
|
||||
|
||||
return tuple(acceptable)
|
||||
|
||||
@property
|
||||
def acceptable_tone_names(self):
|
||||
return tuple([tone.name for tone in self.acceptable_tones])
|
||||
names = [tone.name for tone in self.acceptable_tones]
|
||||
# The root tone is stored internally with sharp spelling (e.g. A#
|
||||
# for Bb) via flat_to_sharp mapping; restore the original flat name.
|
||||
if names and names[0] != self.tone_name:
|
||||
names[0] = self.tone_name
|
||||
return tuple(names)
|
||||
|
||||
def _possible_fingerings(self, *, fretboard):
|
||||
# Check the _possible_cache first
|
||||
key = self._cache_key(fretboard)
|
||||
if key in _possible_cache:
|
||||
return _possible_cache[key]
|
||||
|
||||
def find_fingerings(tone):
|
||||
fingerings = []
|
||||
for j in range(MAX_FRET):
|
||||
@@ -203,13 +307,21 @@ class NamedChord:
|
||||
|
||||
fingering = []
|
||||
for i, tone in enumerate(fretboard.tones):
|
||||
fingering.append(find_fingerings(tone))
|
||||
frets = find_fingerings(tone)
|
||||
# Always allow muting as an option
|
||||
if frets:
|
||||
fingering.append((*frets, -1))
|
||||
else:
|
||||
fingering.append((-1,))
|
||||
|
||||
for i, finger in enumerate(fingering):
|
||||
if finger == ():
|
||||
fingering[i] = (-1,)
|
||||
result = tuple(fingering)
|
||||
|
||||
return tuple(fingering)
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_possible_cache) >= _CACHE_MAX_SIZE:
|
||||
_possible_cache.clear()
|
||||
_possible_cache[key] = result
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def fix_fingering(fingering):
|
||||
@@ -222,39 +334,155 @@ class NamedChord:
|
||||
def fingerings(self, *, fretboard):
|
||||
return tuple(itertools.product(*self._possible_fingerings(fretboard=fretboard)))
|
||||
|
||||
def _cache_key(self, fretboard):
|
||||
"""Return a hashable key for memoization."""
|
||||
return (self.name, tuple(t.full_name for t in fretboard.tones))
|
||||
|
||||
def fingering(self, *, fretboard, multiple=False):
|
||||
# Check cache first
|
||||
key = self._cache_key(fretboard)
|
||||
if multiple:
|
||||
if key in _fingering_multi_cache:
|
||||
return _fingering_multi_cache[key]
|
||||
else:
|
||||
if key in _fingering_cache:
|
||||
return _fingering_cache[key]
|
||||
|
||||
# Check for curated guitar chord overrides in standard tuning
|
||||
tuning = tuple(t.full_name for t in fretboard.tones)
|
||||
if tuning == STANDARD_GUITAR_TUNING and self.name in GUITAR_OVERRIDES:
|
||||
string_names = tuple(t.name for t in fretboard.tones)
|
||||
override = GUITAR_OVERRIDES[self.name]
|
||||
if not multiple:
|
||||
result = Fingering(self.fix_fingering(override), string_names, fretboard=fretboard)
|
||||
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_cache.clear()
|
||||
_fingering_cache[key] = result
|
||||
return result
|
||||
else:
|
||||
result = (Fingering(self.fix_fingering(override), string_names, fretboard=fretboard),)
|
||||
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_multi_cache.clear()
|
||||
_fingering_multi_cache[key] = result
|
||||
return result
|
||||
|
||||
MAX_SPAN = 4 # max fret span for a human hand
|
||||
|
||||
def fingering_score(fingering):
|
||||
def number_of_fingers(fingering):
|
||||
zeros = 0
|
||||
for finger in fingering:
|
||||
if finger == 0:
|
||||
zeros += 1
|
||||
return len(fingering) - zeros
|
||||
score = 0.0
|
||||
fretted = [f for f in fingering if f not in (0, -1)]
|
||||
muted = sum(1 for f in fingering if f == -1)
|
||||
sounding = len(fingering) - muted
|
||||
|
||||
def ascending(fingering):
|
||||
fingering = [f for f in fingering if f != 0]
|
||||
# Must have at least 2 sounding strings
|
||||
if sounding < 2:
|
||||
return -100.0
|
||||
|
||||
return sorted(fingering) == fingering
|
||||
# Hard constraint: fret span must be playable
|
||||
if fretted:
|
||||
span = max(fretted) - min(fretted)
|
||||
if span > MAX_SPAN:
|
||||
return -100.0
|
||||
else:
|
||||
span = 0
|
||||
|
||||
ascending = int(ascending(fingering))
|
||||
finger_count = number_of_fingers(fingering)
|
||||
return ascending + (1 / finger_count)
|
||||
# Check that all chord tones are present in the voicing
|
||||
sounding_names = set()
|
||||
for i, f in enumerate(fingering):
|
||||
if f != -1:
|
||||
sounding_names.add(fretboard.tones[i].add(f).name)
|
||||
required = set(t.name for t in self.acceptable_tones)
|
||||
missing = required - sounding_names
|
||||
score -= len(missing) * 5.0
|
||||
|
||||
# Reward open strings
|
||||
open_strings = sum(1 for f in fingering if f == 0)
|
||||
score += open_strings * 2.0
|
||||
|
||||
# Penalize muted strings, but only mildly
|
||||
score -= muted * 0.3
|
||||
|
||||
# Penalize fret span
|
||||
score -= span * 2.0
|
||||
|
||||
# Penalize high fret positions (prefer open position)
|
||||
if fretted:
|
||||
score -= (sum(fretted) / len(fretted)) * 0.8
|
||||
|
||||
# Barre chord detection: if multiple strings share the same
|
||||
# fret and it's the lowest fret in the shape, one finger can
|
||||
# cover them all — so they cost only 1 finger, not N.
|
||||
# Also check that barre strings are contiguous (no gaps).
|
||||
if fretted:
|
||||
min_fret = min(fretted)
|
||||
barre_indices = [i for i, f in enumerate(fingering) if f == min_fret and f > 0]
|
||||
barre_count = len(barre_indices)
|
||||
|
||||
if barre_count >= 2:
|
||||
unique_higher = len(set(f for f in fretted if f > min_fret))
|
||||
fingers_needed = unique_higher + 1 # 1 for barre
|
||||
# Mild reward for barre efficiency (saves fingers)
|
||||
score += (barre_count - 1) * 0.5
|
||||
else:
|
||||
fingers_needed = len(fretted)
|
||||
else:
|
||||
fingers_needed = 0
|
||||
|
||||
# Penalize fingers needed (max 4 on a guitar)
|
||||
score -= fingers_needed * 0.3
|
||||
if fingers_needed > 4:
|
||||
score -= (fingers_needed - 4) * 5.0
|
||||
|
||||
# Reward root in bass — the lowest sounding string
|
||||
for i in range(len(fingering) - 1, -1, -1):
|
||||
f = fingering[i]
|
||||
if f == -1:
|
||||
continue
|
||||
bass_tone = fretboard.tones[i].add(f)
|
||||
if bass_tone.name == self.tone.name:
|
||||
score += 4.0
|
||||
else:
|
||||
score -= 1.5
|
||||
break
|
||||
|
||||
# Prefer muting from the bass side (contiguous muting)
|
||||
# e.g. xx0232 is good, x0x232 is awkward
|
||||
mute_from_bass = 0
|
||||
for i in range(len(fingering) - 1, -1, -1):
|
||||
if fingering[i] == -1:
|
||||
mute_from_bass += 1
|
||||
else:
|
||||
break
|
||||
interior_mutes = muted - mute_from_bass
|
||||
score -= interior_mutes * 3.0
|
||||
|
||||
return score
|
||||
|
||||
def gen():
|
||||
fingerings = self.fingerings(fretboard=fretboard)
|
||||
score_map = tuple(map(fingering_score, fingerings))
|
||||
max_score = max(score_map)
|
||||
scored = [(fingering_score(f), f) for f in fingerings]
|
||||
max_score = max(s for s, _ in scored)
|
||||
|
||||
for possible_fingering in fingerings:
|
||||
if fingering_score(possible_fingering) == max_score:
|
||||
for s, possible_fingering in scored:
|
||||
if s == max_score:
|
||||
yield possible_fingering
|
||||
|
||||
string_names = tuple(t.name for t in fretboard.tones)
|
||||
best_fingerings = tuple([g for g in gen()])
|
||||
if not multiple:
|
||||
return Fingering(self.fix_fingering(best_fingerings[0]), string_names, fretboard=fretboard)
|
||||
result = Fingering(self.fix_fingering(best_fingerings[0]), string_names, fretboard=fretboard)
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_fingering_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_cache.clear()
|
||||
_fingering_cache[key] = result
|
||||
return result
|
||||
else:
|
||||
return 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) for f in best_fingerings])
|
||||
# Bounded cache: clear entirely if over limit
|
||||
if len(_fingering_multi_cache) >= _CACHE_MAX_SIZE:
|
||||
_fingering_multi_cache.clear()
|
||||
_fingering_multi_cache[key] = result
|
||||
return result
|
||||
|
||||
def tab(self, *, fretboard):
|
||||
"""Render this chord as ASCII guitar tablature.
|
||||
|
||||
+66
-2
@@ -1234,8 +1234,15 @@ class Fretboard:
|
||||
max_name = max(len(t.name) for t in self.tones)
|
||||
lines = []
|
||||
|
||||
# Header with fret numbers
|
||||
header = " " * (max_name + 1) + " ".join(f"{f:<3d}" for f in range(frets + 1))
|
||||
# Each cell is " X |" where X is a note name or dash.
|
||||
# Cell content width is 3 chars (space + 2-char note/dash).
|
||||
# Full cell with separator: 4 chars.
|
||||
# Header must align fret numbers to the center of each cell.
|
||||
header_parts = []
|
||||
for f in range(frets + 1):
|
||||
header_parts.append(f"{f:>2} ")
|
||||
# Offset header to align with cell content (after "X|" prefix)
|
||||
header = " " * (max_name + 2) + " ".join(header_parts)
|
||||
lines.append(header)
|
||||
|
||||
for tone in self.tones:
|
||||
@@ -1270,6 +1277,63 @@ class Fretboard:
|
||||
from .charts import CHARTS
|
||||
return CHARTS[system][name].fingering(fretboard=self)
|
||||
|
||||
def __getitem__(self, name: str) -> "Fingering":
|
||||
"""Shorthand for :meth:`chord` — ``fb["G"]`` equals ``fb.chord("G")``.
|
||||
|
||||
Args:
|
||||
name: Chord name like ``"G"``, ``"Am7"``, ``"Bb"``.
|
||||
|
||||
Returns:
|
||||
A :class:`Fingering` for that chord on this fretboard.
|
||||
|
||||
Example::
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> fb["G"]
|
||||
Fingering(e=3, B=0, G=0, D=0, A=2, E=3)
|
||||
"""
|
||||
return self.chord(name)
|
||||
|
||||
def tab(self, name: str, *, system: str = "western") -> str:
|
||||
"""Look up a chord by name and return its ASCII tablature.
|
||||
|
||||
Args:
|
||||
name: Chord name like ``"G"``, ``"Am7"``, ``"Bb"``.
|
||||
system: Tonal system to use (default ``"western"``).
|
||||
|
||||
Returns:
|
||||
A multi-line string showing the chord as tablature.
|
||||
|
||||
Example::
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> print(fb.tab("Am"))
|
||||
A minor
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--2--
|
||||
D|--2--
|
||||
A|--0--
|
||||
E|--0--
|
||||
"""
|
||||
return self.chord(name, system=system).tab()
|
||||
|
||||
def chart(self, *, system: str = "western") -> dict:
|
||||
"""Generate fingerings for every chord in the given system.
|
||||
|
||||
Returns:
|
||||
A dict mapping chord names to :class:`Fingering` objects.
|
||||
|
||||
Example::
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> chart = fb.chart()
|
||||
>>> chart["Am7"]
|
||||
Fingering(e=0, B=1, G=0, D=2, A=0, E=0)
|
||||
"""
|
||||
from .charts import charts_for_fretboard, CHARTS
|
||||
return charts_for_fretboard(chart=CHARTS[system], fretboard=self)
|
||||
|
||||
def fingering(self, *positions: int) -> "Fingering":
|
||||
"""Apply fret positions to each string, returning a Fingering.
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from enum import Enum
|
||||
import time
|
||||
|
||||
import numpy
|
||||
import scipy.signal
|
||||
import sounddevice as sd
|
||||
@@ -124,3 +126,24 @@ def save(tone_or_chord, path, temperament="equal", synth=Synth.SINE, t=1_000):
|
||||
# Convert to 16-bit PCM
|
||||
pcm = (normalized * 32767).astype(numpy.int16)
|
||||
scipy.io.wavfile.write(path, SAMPLE_RATE, pcm)
|
||||
|
||||
|
||||
def play_progression(chords, *, t=1000, synth=Synth.SINE, gap=100):
|
||||
"""Play a list of chords in sequence.
|
||||
|
||||
Args:
|
||||
chords: List of Chord objects to play in order.
|
||||
t: Duration of each chord in milliseconds.
|
||||
synth: Waveform type (Synth.SINE, etc). Defaults to sine.
|
||||
gap: Silence between chords in milliseconds.
|
||||
|
||||
Example::
|
||||
|
||||
>>> from pytheory import Key, play_progression
|
||||
>>> chords = Key("C", "major").progression("I", "V", "vi", "IV")
|
||||
>>> play_progression(chords, t=800)
|
||||
"""
|
||||
for i, chord in enumerate(chords):
|
||||
play(chord, synth=synth, t=t)
|
||||
if gap > 0 and i < len(chords) - 1:
|
||||
time.sleep(gap / 1000.0)
|
||||
|
||||
+27
-3
@@ -659,27 +659,51 @@ class TonedScale:
|
||||
"""Tuple of all available scale names in this system."""
|
||||
return tuple(self._scales.keys())
|
||||
|
||||
@staticmethod
|
||||
def _should_prefer_flats(tones: list) -> bool:
|
||||
"""Determine if a scale should use flat spellings.
|
||||
|
||||
Uses the "no duplicate letters" rule: build the scale with sharps
|
||||
first, and if any letter name appears twice (excluding the octave
|
||||
repeat at the end), try flats instead. This correctly handles all
|
||||
keys on the circle of fifths.
|
||||
"""
|
||||
# Exclude the last tone (octave repeat of the tonic)
|
||||
unique_tones = tones[:-1] if len(tones) > 1 else tones
|
||||
letters = [t.name[0] for t in unique_tones]
|
||||
return len(letters) != len(set(letters))
|
||||
|
||||
@property
|
||||
def _scales(self) -> dict[str, Scale]:
|
||||
"""Lazily computed (and cached) mapping of scale names to Scale objects."""
|
||||
if self._cached_scales is not None:
|
||||
return self._cached_scales
|
||||
|
||||
# Also check if tonic itself is a flat (always prefer flats then)
|
||||
tonic_is_flat = "b" in self.tonic.name and self.tonic.name != "B"
|
||||
|
||||
scales = {}
|
||||
|
||||
for scale_type in self.system.scales:
|
||||
for scale in self.system.scales[scale_type]:
|
||||
|
||||
working_scale = []
|
||||
reference_scale = self.system.scales[scale_type][scale]["intervals"]
|
||||
|
||||
# First pass: build with sharps (default)
|
||||
working_scale = [self.tonic]
|
||||
current_tone = self.tonic
|
||||
working_scale.append(current_tone)
|
||||
|
||||
for interval in reference_scale:
|
||||
current_tone = current_tone.add(interval)
|
||||
working_scale.append(current_tone)
|
||||
|
||||
# Check if we need flats (duplicate letter names)
|
||||
if tonic_is_flat or self._should_prefer_flats(working_scale):
|
||||
working_scale = [self.tonic]
|
||||
current_tone = self.tonic
|
||||
for interval in reference_scale:
|
||||
current_tone = current_tone.add(interval, prefer_flats=True)
|
||||
working_scale.append(current_tone)
|
||||
|
||||
scales[scale] = Scale(tones=tuple(working_scale))
|
||||
|
||||
self._cached_scales = scales
|
||||
|
||||
+12
-4
@@ -313,18 +313,24 @@ class Tone:
|
||||
return klass.from_index(index, octave=octave, system=system)
|
||||
|
||||
@classmethod
|
||||
def from_index(klass, i: int, *, octave: int, system: object) -> Tone:
|
||||
def from_index(klass, i: int, *, octave: int, system: object, prefer_flats: bool = False) -> Tone:
|
||||
"""Create a Tone from its index within a tuning system.
|
||||
|
||||
Args:
|
||||
i: The index of the tone in the system's tone list.
|
||||
octave: The octave number.
|
||||
system: The ``ToneSystem`` instance.
|
||||
prefer_flats: If True and the tone has a flat spelling,
|
||||
use it instead of the default sharp spelling.
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
tone = system.tones[i].name
|
||||
tone_names = system.tone_names[i]
|
||||
if prefer_flats and len(tone_names) > 1:
|
||||
tone = tone_names[1] # flat spelling (e.g. "Bb")
|
||||
else:
|
||||
tone = tone_names[0] # sharp spelling (e.g. "A#")
|
||||
return klass(name=tone, octave=octave, system=system)
|
||||
|
||||
@property
|
||||
@@ -375,17 +381,19 @@ class Tone:
|
||||
|
||||
return (new_index, new_octave)
|
||||
|
||||
def add(self, interval: int) -> Tone:
|
||||
def add(self, interval: int, *, prefer_flats: bool = False) -> Tone:
|
||||
"""Return a new Tone that is *interval* semitones above this one.
|
||||
|
||||
Args:
|
||||
interval: Number of semitones to add (positive = up).
|
||||
prefer_flats: If True, use flat spellings (Bb, Eb) instead
|
||||
of sharp spellings (A#, D#) for accidentals.
|
||||
|
||||
Returns:
|
||||
A new ``Tone`` instance.
|
||||
"""
|
||||
index, octave = self._math(interval)
|
||||
return self.from_index(index, octave=octave, system=self.system)
|
||||
return self.from_index(index, octave=octave, system=self.system, prefer_flats=prefer_flats)
|
||||
|
||||
def subtract(self, interval: int) -> Tone:
|
||||
"""Return a new Tone that is *interval* semitones below this one.
|
||||
|
||||
+71
-25
@@ -238,8 +238,8 @@ def test_c_minor_scale():
|
||||
c = TonedScale(tonic="C4")
|
||||
minor = c["minor"]
|
||||
names = [t.name for t in minor.tones]
|
||||
# C D Eb F G Ab Bb C (using sharps: D#, G#, A#)
|
||||
assert names == ["C", "D", "D#", "F", "G", "G#", "A#", "C"]
|
||||
# C D Eb F G Ab Bb C (using flats for flat keys)
|
||||
assert names == ["C", "D", "Eb", "F", "G", "Ab", "Bb", "C"]
|
||||
|
||||
|
||||
def test_c_harmonic_minor_scale():
|
||||
@@ -247,7 +247,7 @@ def test_c_harmonic_minor_scale():
|
||||
hminor = c["harmonic minor"]
|
||||
names = [t.name for t in hminor.tones]
|
||||
# C D Eb F G Ab B C (raised 7th)
|
||||
assert names == ["C", "D", "D#", "F", "G", "G#", "B", "C"]
|
||||
assert names == ["C", "D", "Eb", "F", "G", "Ab", "B", "C"]
|
||||
|
||||
|
||||
def test_g_major_scale():
|
||||
@@ -308,7 +308,7 @@ def test_c_dorian():
|
||||
dorian = c["dorian"]
|
||||
names = [t.name for t in dorian.tones]
|
||||
# Dorian: W H W W W H W → C D Eb F G A Bb C
|
||||
assert names == ["C", "D", "D#", "F", "G", "A", "A#", "C"]
|
||||
assert names == ["C", "D", "Eb", "F", "G", "A", "Bb", "C"]
|
||||
|
||||
|
||||
def test_c_phrygian():
|
||||
@@ -316,7 +316,7 @@ def test_c_phrygian():
|
||||
phrygian = c["phrygian"]
|
||||
names = [t.name for t in phrygian.tones]
|
||||
# Phrygian: H W W W H W W → C Db Eb F G Ab Bb C
|
||||
assert names == ["C", "C#", "D#", "F", "G", "G#", "A#", "C"]
|
||||
assert names == ["C", "Db", "Eb", "F", "G", "Ab", "Bb", "C"]
|
||||
|
||||
|
||||
def test_c_lydian():
|
||||
@@ -332,7 +332,7 @@ def test_c_mixolydian():
|
||||
mixolydian = c["mixolydian"]
|
||||
names = [t.name for t in mixolydian.tones]
|
||||
# Mixolydian: W W H W W H W → C D E F G A Bb C
|
||||
assert names == ["C", "D", "E", "F", "G", "A", "A#", "C"]
|
||||
assert names == ["C", "D", "E", "F", "G", "A", "Bb", "C"]
|
||||
|
||||
|
||||
def test_c_locrian():
|
||||
@@ -340,7 +340,7 @@ def test_c_locrian():
|
||||
locrian = c["locrian"]
|
||||
names = [t.name for t in locrian.tones]
|
||||
# Locrian: H W W H W W W → C Db Eb F Gb Ab Bb C
|
||||
assert names == ["C", "C#", "D#", "F", "F#", "G#", "A#", "C"]
|
||||
assert names == ["C", "Db", "Eb", "F", "Gb", "Ab", "Bb", "C"]
|
||||
|
||||
|
||||
# ── Chords ───────────────────────────────────────────────────────────────────
|
||||
@@ -417,7 +417,7 @@ def test_named_chord_c_minor_tones():
|
||||
cm = NamedChord(tone_name="C", quality="m")
|
||||
names = cm.acceptable_tone_names
|
||||
assert "C" in names
|
||||
assert "D#" in names # Eb enharmonic
|
||||
assert "Eb" in names # minor 3rd
|
||||
assert "G" in names
|
||||
|
||||
|
||||
@@ -435,24 +435,24 @@ def test_named_chord_dominant_7th():
|
||||
assert "C" in names
|
||||
assert "E" in names # major 3rd
|
||||
assert "G" in names # perfect 5th
|
||||
assert "A#" in names # minor 7th (Bb)
|
||||
assert "Bb" in names # minor 7th
|
||||
|
||||
|
||||
def test_named_chord_diminished():
|
||||
cdim = NamedChord(tone_name="C", quality="dim")
|
||||
names = cdim.acceptable_tone_names
|
||||
assert "C" in names
|
||||
assert "D#" in names # minor 3rd (Eb)
|
||||
assert "F#" in names # diminished 5th (Gb)
|
||||
assert "Eb" in names # minor 3rd
|
||||
assert "Gb" in names # diminished 5th
|
||||
|
||||
|
||||
def test_named_chord_minor_7th():
|
||||
cm7 = NamedChord(tone_name="C", quality="m7")
|
||||
names = cm7.acceptable_tone_names
|
||||
assert "C" in names
|
||||
assert "D#" in names # minor 3rd
|
||||
assert "Eb" in names # minor 3rd
|
||||
assert "G" in names # perfect 5th
|
||||
assert "A#" in names # minor 7th
|
||||
assert "Bb" in names # minor 7th
|
||||
|
||||
|
||||
def test_named_chord_major_7th():
|
||||
@@ -525,6 +525,7 @@ def test_chord_fingering_em(guitar_fretboard):
|
||||
assert zeros >= 3
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_chord_fingering_all_western_chords(guitar_fretboard):
|
||||
"""Every chord in the western chart should produce a valid fingering."""
|
||||
for name, chord in CHARTS["western"].items():
|
||||
@@ -975,7 +976,7 @@ def test_f_major_scale():
|
||||
f = TonedScale(tonic="F4")
|
||||
major = f["major"]
|
||||
names = [t.name for t in major.tones]
|
||||
assert names == ["F", "G", "A", "A#", "C", "D", "E", "F"]
|
||||
assert names == ["F", "G", "A", "Bb", "C", "D", "E", "F"]
|
||||
|
||||
|
||||
def test_a_minor_scale():
|
||||
@@ -1257,7 +1258,7 @@ def test_named_chord_m6_tones():
|
||||
cm6 = NamedChord(tone_name="C", quality="m6")
|
||||
names = cm6.acceptable_tone_names
|
||||
assert "C" in names
|
||||
assert "D#" in names # minor 3rd
|
||||
assert "Eb" in names # minor 3rd
|
||||
assert "G" in names # perfect 5th
|
||||
assert "A" in names # major 6th
|
||||
assert len(names) == 4
|
||||
@@ -1267,9 +1268,9 @@ def test_named_chord_m9_tones():
|
||||
cm9 = NamedChord(tone_name="C", quality="m9")
|
||||
names = cm9.acceptable_tone_names
|
||||
assert "C" in names
|
||||
assert "D#" in names # minor 3rd
|
||||
assert "Eb" in names # minor 3rd
|
||||
assert "G" in names # perfect 5th
|
||||
assert "A#" in names # minor 7th
|
||||
assert "Bb" in names # minor 7th
|
||||
assert "D" in names # major 9th
|
||||
assert len(names) == 5
|
||||
|
||||
@@ -1291,7 +1292,7 @@ def test_named_chord_9_tones():
|
||||
assert "C" in names
|
||||
assert "E" in names # major 3rd
|
||||
assert "G" in names # perfect 5th
|
||||
assert "A#" in names # minor 7th
|
||||
assert "Bb" in names # minor 7th
|
||||
assert "D" in names # major 9th
|
||||
assert len(names) == 5
|
||||
|
||||
@@ -1330,6 +1331,7 @@ def test_charts_all_qualities_present():
|
||||
assert len(matching) > 0, f"No chords with quality '{quality}'"
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_charts_for_fretboard(guitar_fretboard):
|
||||
result = charts_for_fretboard(fretboard=guitar_fretboard)
|
||||
assert len(result) == len(CHARTS["western"])
|
||||
@@ -1337,6 +1339,7 @@ def test_charts_for_fretboard(guitar_fretboard):
|
||||
assert len(fingering) == 6, f"{name} has wrong fingering length"
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_charts_fingering_values_in_range(guitar_fretboard):
|
||||
"""All fret values should be 0-6 or None (muted)."""
|
||||
for name, chord in CHARTS["western"].items():
|
||||
@@ -2296,7 +2299,7 @@ def test_japanese_hirajoshi():
|
||||
c = TonedScale(tonic="C4", system=SYSTEMS["japanese"])
|
||||
h = c["hirajoshi"]
|
||||
names = [t.name for t in h]
|
||||
assert names == ["C", "D", "D#", "G", "G#", "C"]
|
||||
assert names == ["C", "D", "Eb", "G", "Ab", "C"]
|
||||
|
||||
|
||||
def test_japanese_in_scale():
|
||||
@@ -2304,7 +2307,7 @@ def test_japanese_in_scale():
|
||||
c = TonedScale(tonic="C4", system=SYSTEMS["japanese"])
|
||||
s = c["in"]
|
||||
names = [t.name for t in s]
|
||||
assert names == ["C", "C#", "F", "G", "G#", "C"]
|
||||
assert names == ["C", "Db", "F", "G", "Ab", "C"]
|
||||
|
||||
|
||||
def test_japanese_yo_scale():
|
||||
@@ -2320,7 +2323,7 @@ def test_japanese_iwato():
|
||||
c = TonedScale(tonic="C4", system=SYSTEMS["japanese"])
|
||||
s = c["iwato"]
|
||||
names = [t.name for t in s]
|
||||
assert names == ["C", "C#", "F", "F#", "A#", "C"]
|
||||
assert names == ["C", "Db", "F", "Gb", "Bb", "C"]
|
||||
|
||||
|
||||
def test_japanese_kumoi():
|
||||
@@ -2328,7 +2331,7 @@ def test_japanese_kumoi():
|
||||
c = TonedScale(tonic="C4", system=SYSTEMS["japanese"])
|
||||
s = c["kumoi"]
|
||||
names = [t.name for t in s]
|
||||
assert names == ["C", "D", "D#", "G", "A", "C"]
|
||||
assert names == ["C", "D", "Eb", "G", "A", "C"]
|
||||
|
||||
|
||||
def test_japanese_ritsu():
|
||||
@@ -2336,7 +2339,7 @@ def test_japanese_ritsu():
|
||||
c = TonedScale(tonic="C4", system=SYSTEMS["japanese"])
|
||||
s = c["ritsu"]
|
||||
names = [t.name for t in s]
|
||||
assert names == ["C", "D", "D#", "F", "G", "A", "A#", "C"]
|
||||
assert names == ["C", "D", "Eb", "F", "G", "A", "Bb", "C"]
|
||||
|
||||
|
||||
def test_japanese_all_scales_available():
|
||||
@@ -2382,7 +2385,7 @@ def test_blues_scale():
|
||||
c = TonedScale(tonic="C4", system=SYSTEMS["blues"])
|
||||
s = c["blues"]
|
||||
names = s.note_names
|
||||
assert names == ["C", "D#", "F", "F#", "G", "A#", "C"]
|
||||
assert names == ["C", "Eb", "F", "Gb", "G", "Bb", "C"]
|
||||
assert len(names) == 7 # 6 notes + octave
|
||||
|
||||
|
||||
@@ -2622,7 +2625,7 @@ def test_tension_empty():
|
||||
|
||||
def test_version():
|
||||
import pytheory
|
||||
assert pytheory.__version__ == "0.6.1"
|
||||
assert pytheory.__version__
|
||||
|
||||
|
||||
def test_all_exports():
|
||||
@@ -3649,6 +3652,49 @@ def test_charts_muted_string():
|
||||
assert fixed == (0, None, 2)
|
||||
|
||||
|
||||
def test_fretboard_chord_method():
|
||||
"""Fretboard.chord() looks up a chord by name."""
|
||||
fb = Fretboard.guitar()
|
||||
f = fb.chord("G")
|
||||
assert f.identify() == "G major"
|
||||
assert len(f) == 6
|
||||
|
||||
|
||||
def test_fretboard_chord_system_kwarg():
|
||||
"""Fretboard.chord() accepts a system keyword argument."""
|
||||
fb = Fretboard.guitar()
|
||||
f = fb.chord("Am", system="western")
|
||||
assert f.identify() == "A minor"
|
||||
|
||||
|
||||
def test_fretboard_tab_method():
|
||||
"""Fretboard.tab() returns ASCII tablature."""
|
||||
fb = Fretboard.guitar()
|
||||
tab = fb.tab("C")
|
||||
assert "C major" in tab
|
||||
assert "e|" in tab
|
||||
assert "E|" in tab
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_fretboard_chart_method():
|
||||
"""Fretboard.chart() generates all fingerings."""
|
||||
fb = Fretboard.guitar()
|
||||
chart = fb.chart()
|
||||
assert "C" in chart
|
||||
assert "Am7" in chart
|
||||
assert chart["C"].identify() == "C major"
|
||||
|
||||
|
||||
def test_fingering_tab_method():
|
||||
"""Fingering.tab() renders ASCII tablature."""
|
||||
fb = Fretboard.guitar()
|
||||
f = fb.chord("Em")
|
||||
tab = f.tab()
|
||||
assert "E minor" in tab
|
||||
assert "e|" in tab
|
||||
|
||||
|
||||
# ── Flat note support ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_flat_tone_from_string():
|
||||
|
||||
Reference in New Issue
Block a user