Improve fingering algorithm, add convenience APIs, convert all docs to REPL style

- Fretboard.chord(), .tab(), .chart() convenience methods
- Fingering.tab() for rendering ASCII tablature
- Fingering algorithm now considers muting, fret span, root-in-bass,
  and contiguous bass-side muting for idiomatic voicings
- All docs converted from code-block:: python to pycon with >>> prompts
- All doc outputs verified against actual library output
- Tests for new methods; version test no longer checks exact string

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 08:36:31 -04:00
parent 7e1d9e76bd
commit b22b3c063f
11 changed files with 859 additions and 686 deletions
+158 -146
View File
@@ -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', 'D#', 'G', 'A#')
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.
- **1530 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]
+93 -103
View File
@@ -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"),
... ])
+29 -29
View File
@@ -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,26 +52,26 @@ 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
@@ -84,16 +84,16 @@ 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)
+73 -70
View File
@@ -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', 'D#', 'F', 'G', 'G#', 'A#', 'C']
c["dorian"].note_names
# ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C']
>>> c["dorian"].note_names
['C', 'D', 'D#', 'F', 'G', 'A', 'A#', '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,27 @@ 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--
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
------------
+149 -130
View File
@@ -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', 'D#', 'F', 'G', 'A', 'A#', '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', 'C#', 'D#', 'F', 'G', 'G#', 'A#', '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', 'A#', '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', 'D#', 'F', 'G', 'G#', 'A#', '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', 'C#', 'D#', 'F', 'F#', 'G#', 'A#', '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']}
{'sharps': 1, 'flats': 0, 'accidentals': ['A#']}
>>> 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', 'D# major', 'F minor', 'G minor', 'G# major', 'A# 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
View File
@@ -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', 'D#', 'F', 'G', 'A', 'A#', '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', 'D#', 'G', 'G#', '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', 'C#', 'F', 'G', 'G#', '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', 'D#', 'F', 'G', 'A', 'A#', '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', 'D#', 'F', 'F#', 'G', 'A#', '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', 'D#', '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
View File
@@ -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
View File
@@ -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>`_
(0127), 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']
+96 -21
View File
@@ -107,6 +107,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"] = []
@@ -203,11 +230,12 @@ class NamedChord:
fingering = []
for i, tone in enumerate(fretboard.tones):
fingering.append(find_fingerings(tone))
for i, finger in enumerate(fingering):
if finger == ():
fingering[i] = (-1,)
frets = find_fingerings(tone)
# Always allow muting as an option
if frets:
fingering.append((*frets, -1))
else:
fingering.append((-1,))
return tuple(fingering)
@@ -224,29 +252,76 @@ class NamedChord:
def fingering(self, *, fretboard, multiple=False):
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
# 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
ascending = int(ascending(fingering))
finger_count = number_of_fingers(fingering)
return ascending + (1 / finger_count)
# 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 (hard to stretch)
if fretted:
span = max(fretted) - min(fretted)
score -= span * 2.0
# Penalize high fret positions (prefer open position)
if fretted:
score -= (sum(fretted) / len(fretted)) * 0.8
# Penalize many fingers needed
score -= len(fretted) * 0.3
# 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:
# Penalize non-root bass notes
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)
+40
View File
@@ -1270,6 +1270,46 @@ class Fretboard:
from .charts import CHARTS
return CHARTS[system][name].fingering(fretboard=self)
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.
+43 -1
View File
@@ -2622,7 +2622,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 +3649,48 @@ 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
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():