Compare commits

...

10 Commits

Author SHA1 Message Date
kennethreitz 5aed586187 v0.8.2: Flat spellings in CHARTS acceptable_tone_names
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:52:50 -04:00
kennethreitz 09d90b3425 Use flat spellings in CHARTS acceptable_tone_names
NamedChord.acceptable_tones now uses prefer_flats based on circle-of-fifths
conventions. Cm7 shows (C, Eb, G, Bb) instead of (C, D#, G, A#).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:43:55 -04:00
kennethreitz 96131da59c v0.8.1: Musically correct flat spellings in flat keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:24:06 -04:00
kennethreitz d2058668a6 Use musically correct flat spellings in flat keys
Flat keys now display flats (Bb, Eb, Ab) instead of sharps (A#, D#, G#).
Uses the "no duplicate letter names" rule: if building a scale with
sharps produces two notes with the same letter (e.g. C and C# in C minor),
the scale is rebuilt with flat spellings instead.

- Tone.add() and Tone.from_index() accept prefer_flats parameter
- TonedScale detects flat vs sharp per-scale automatically
- F major: Bb (not A#), Eb major: Ab Bb (not G# A#), etc.
- All tests and docs updated to match

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:22:39 -04:00
kennethreitz a5ffdc6104 Expand cookbook, fix scale_diagram alignment, add play_progression
- 11 new cookbook recipes: circle of fifths, voice leading, tension
  analysis, tritone substitution, key signatures/detection, relative
  and parallel keys, borrowed chords, secondary dominants, overtones,
  enharmonics, world scales, guitar scale visualization
- Fix scale_diagram header alignment for 2-digit fret numbers
- play_progression() for sequencing chord playback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:05:41 -04:00
kennethreitz 724a0df7b5 Extract pentatonic variable on homepage for readability
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:57:18 -04:00
kennethreitz 4750061b87 v0.8.0: Scale diagrams, cookbook, progression playback
- scale_diagram() showcased on homepage and quickstart
- New cookbook page: analyze a song, 12-bar blues, find chords in a key,
  compare scales, guitar chord chart, explore intervals
- play_progression() for sequencing chord playback with gaps
- Scale and Note aliases exported
- Version bump to 0.8.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:56:43 -04:00
kennethreitz d53d8b60dd API ergonomics, curated fingerings, bounded caching, slow test markers
- Fretboard["G"] shorthand via __getitem__
- Chord repr now shows <Chord C major> format
- Scale = TonedScale and Note = Tone aliases
- GUITAR_OVERRIDES dict with 15 curated standard chord shapes
  (F barre 133211, B barre x24442, etc.)
- Bounded caches (max 1024 entries) for fingerings and possible_fingerings
- @pytest.mark.slow on 4 chart-generation tests; fast suite runs in 2s
- Highlights section moved above CLI examples on docs homepage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:52:17 -04:00
kennethreitz de1db0aa8d Add fingering memoization, barre detection, and 4-fret span constraint
- Cache fingering results by chord name + tuning for instant repeat lookups
- chart() goes from ~53s to <1ms on second call
- Barre chord detection: when multiple strings share the lowest fret,
  count them as 1 finger instead of N
- Hard 4-fret span constraint rejects unplayable voicings
- Penalize shapes requiring more than 4 fingers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:42:45 -04:00
kennethreitz b22b3c063f 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>
2026-03-23 08:36:31 -04:00
18 changed files with 1576 additions and 754 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', '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.
- **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]
+366
View File
@@ -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
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"),
... ])
+49 -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,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
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', '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
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', '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
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', '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
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']
+27 -16
View File
@@ -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
View File
@@ -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"]
+6 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+23
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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():