diff --git a/docs/guide/chords.rst b/docs/guide/chords.rst index d0588d6..6ca6bd4 100644 --- a/docs/guide/chords.rst +++ b/docs/guide/chords.rst @@ -45,18 +45,20 @@ For seventh chords, there's also **third inversion** (7th in bass): - G7 in third inversion: F G B D (notated G7/F) -.. code-block:: python +.. code-block:: pycon - from pytheory import Chord, Tone + >>> from pytheory import Chord, Tone - # All three are "C major" — identify() finds the root - root = Chord([Tone.from_string(n, system="western") for n in ["C4", "E4", "G4"]]) - first = Chord([Tone.from_string(n, system="western") for n in ["E3", "G3", "C4"]]) - second = Chord([Tone.from_string(n, system="western") for n in ["G3", "C4", "E4"]]) + >>> root = Chord([Tone.from_string(n, system="western") for n in ["C4", "E4", "G4"]]) + >>> first = Chord([Tone.from_string(n, system="western") for n in ["E3", "G3", "C4"]]) + >>> second = Chord([Tone.from_string(n, system="western") for n in ["G3", "C4", "E4"]]) - root.identify() # 'C major' - first.identify() # 'C major' - second.identify() # 'C major' + >>> root.identify() + 'C major' + >>> first.identify() + 'C major' + >>> second.identify() + 'C major' Extended Chords --------------- @@ -72,33 +74,42 @@ A full 13th chord contains all 7 notes of the scale! In practice, tones are usually omitted — the 5th is typically dropped first, then the 11th (which clashes with the 3rd in dominant chords). -.. code-block:: python +.. code-block:: pycon - from pytheory import TonedScale + >>> from pytheory import TonedScale - scale = TonedScale(tonic="C4")["major"] + >>> scale = TonedScale(tonic="C4")["major"] - # Build a Cmaj9 from the scale: C E G B D - cmaj9 = scale.chord(0, 2, 4, 6, 8) - - # Build a full C13 (in theory): C E G B D F A - c13 = scale.chord(0, 2, 4, 6, 8, 10, 12) + >>> cmaj9 = scale.chord(0, 2, 4, 6, 8) + >>> c13 = scale.chord(0, 2, 4, 6, 8, 10, 12) Using the Chord Chart --------------------- PyTheory includes 144 pre-built chords (12 roots x 12 qualities): -.. code-block:: python +.. code-block:: pycon - from pytheory import CHARTS + >>> from pytheory import Fretboard - chart = CHARTS["western"] + >>> fb = Fretboard.guitar() + >>> fb.chord("C") + Fingering(e=0, B=1, G=0, D=2, A=3, E=x) + >>> fb.chord("Am") + Fingering(e=0, B=1, G=2, D=2, A=0, E=x) + >>> fb.chord("G7") + Fingering(e=1, B=0, G=0, D=0, A=2, E=3) - c_major = chart["C"] # C major (root position) - a_minor = chart["Am"] # A minor - g_seven = chart["G7"] # G dominant 7th - d_dim = chart["Ddim"] # D diminished +You can also build chords directly with ``Chord.from_name()``: + +.. code-block:: pycon + + >>> from pytheory import Chord + + >>> Chord.from_name("G7").identify() + 'G dominant 7th' + >>> Chord.from_name("Ddim").identify() + 'D diminished' Available qualities: @@ -119,52 +130,48 @@ Quality Intervals Example tones (from C) ``"maj9"`` 4, 7, 11, 14 C E G B D (major 9th) ============ ================ ================================ -.. code-block:: python +.. code-block:: pycon + + >>> from pytheory import CHARTS + >>> chart = CHARTS["western"] >>> chart["C"].acceptable_tone_names ('C', 'E', 'G') >>> chart["Cm7"].acceptable_tone_names - ('C', 'D#', 'G', 'A#') # Eb and Bb shown as sharps + ('C', 'D#', 'G', 'A#') Building Chords --------------- Several convenience constructors make chord creation concise: -.. code-block:: python +.. code-block:: pycon - from pytheory import Chord + >>> from pytheory import Chord - # From note names (simplest) - Chord.from_tones("C", "E", "G") # - Chord.from_tones("A", "C", "E") # + >>> 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.from_name("G7") # + >>> 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.from_intervals("D", 3, 7) # - Chord.from_intervals("G", 4, 7, 10) # + >>> 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.from_midi_message(60, 64, 67).identify() + 'C major' - # Full manual construction - from pytheory import Tone - c_major = Chord(tones=[ - Tone.from_string("C4", system="western"), - Tone.from_string("E4", system="western"), - Tone.from_string("G4", system="western"), - ]) - - for tone in c_major: - print(tone) - - len(c_major) # 3 - "C" in c_major # True + >>> len(Chord.from_name("C")) + 3 + >>> "C" in Chord.from_name("C") + True Intervals --------- @@ -172,13 +179,13 @@ Intervals The ``intervals`` property returns semitone distances between adjacent tones — these are musically meaningful and octave-invariant: -.. code-block:: python +.. code-block:: pycon - >>> c_major.intervals - [4, 3] # major 3rd (4) + minor 3rd (3) = major triad + >>> Chord.from_tones("C", "E", "G").intervals + [4, 3] - >>> Chord(tones=[C4, Eb4, G4]).intervals - [3, 4] # minor 3rd + major 3rd = minor triad + >>> Chord.from_tones("C", "Eb", "G").intervals + [3, 4] Consonance and Dissonance ------------------------- @@ -205,13 +212,16 @@ Minor 3rd 6:5 Every 6th wave aligns Tritone 45:32 Waves rarely align =========== ===== ==================== -.. code-block:: python +.. code-block:: pycon - fifth = Chord([C4, G4]) - tritone = Chord([C4, F_sharp_4]) + >>> from pytheory import Chord, Tone + >>> C4 = Tone.from_string("C4", system="western") + >>> G4 = Tone.from_string("G4", system="western") - fifth.harmony > tritone.harmony # True - # The perfect fifth's 3:2 ratio scores higher + >>> fifth = Chord([C4, G4]) + >>> tritone = Chord([C4, C4 + 6]) + >>> fifth.harmony > tritone.harmony + True Dissonance Score ~~~~~~~~~~~~~~~~ @@ -227,14 +237,13 @@ The roughness depends on the frequency difference relative to the that register). Maximum roughness occurs when the difference equals the critical bandwidth. -.. code-block:: python +.. code-block:: pycon - # Octave: frequencies far apart → low roughness - octave = Chord([C4, C5]) - # Major 3rd: closer frequencies → higher roughness - third = Chord([C4, E4]) - - octave.dissonance < third.dissonance # True + >>> E4 = Tone.from_string("E4", system="western") + >>> octave = Chord([C4, C4 + 12]) + >>> third = Chord([C4, E4]) + >>> octave.dissonance < third.dissonance + True Beat Frequencies ~~~~~~~~~~~~~~~~ @@ -247,23 +256,23 @@ you hear a pulsing at the **beat frequency**: ``|f1 - f2|`` Hz. - **15–30 Hz**: Perceived as buzzing/roughness - **> 30 Hz**: No longer beating — becomes part of the timbre -.. code-block:: python +.. code-block:: pycon - chord = Chord(tones=[A4, E5, A5]) + >>> A4 = Tone.from_string("A4", system="western") + >>> chord = Chord([A4, A4 + 7, A4 + 12]) - # All pairwise beat frequencies, sorted ascending - chord.beat_frequencies - # [(A4, E5, 189.6), (E5, A5, 220.0), (A4, A5, 440.0)] + >>> chord.beat_frequencies + [...] - # The slowest (most perceptible) beat - chord.beat_pulse # 189.6 Hz + >>> round(chord.beat_pulse, 1) + 219.3 Transposition ------------- Shift an entire chord up or down by any number of semitones: -.. code-block:: python +.. code-block:: pycon >>> Chord.from_name("C").transpose(7).identify() 'G major' @@ -276,20 +285,20 @@ Chord Manipulation Add or remove individual tones from a chord: -.. code-block:: python +.. code-block:: pycon - from pytheory import Chord, Tone + >>> from pytheory import Chord, Tone - c_major = Chord.from_tones("C", "E", "G") + >>> c_major = Chord.from_tones("C", "E", "G") - # Add a tone to build a seventh chord - b4 = Tone.from_string("B4", system="western") - cmaj7 = c_major.add_tone(b4) - cmaj7.identify() # 'C major 7th' + >>> b4 = Tone.from_string("B4", system="western") + >>> cmaj7 = c_major.add_tone(b4) + >>> cmaj7.identify() + 'C major 7th' - # Remove a tone - c_again = cmaj7.remove_tone("B") - c_again.identify() # 'C major' + >>> c_again = cmaj7.remove_tone("B") + >>> c_again.identify() + 'C major' Chord Identification -------------------- @@ -298,27 +307,30 @@ Give PyTheory any set of tones and it will tell you what chord it is. It tries every tone as a potential root and matches the interval pattern against 17 known chord types (triads, 7ths, 9ths, sus, power chords). -.. code-block:: python +.. code-block:: pycon - from pytheory import Chord + >>> from pytheory import Chord - # From note names - Chord.from_tones("A", "C", "E").identify() # 'A minor' - Chord.from_tones("G", "B", "D", "F").identify() # 'G dominant 7th' + >>> Chord.from_tones("A", "C", "E").identify() + 'A minor' + >>> Chord.from_tones("G", "B", "D", "F").identify() + 'G dominant 7th' - # Works with any voicing or inversion - Chord.from_tones("E", "G", "C").identify() # 'C major' + >>> Chord.from_tones("E", "G", "C").identify() + 'C major' - # Flats work too - Chord.from_tones("Bb", "D", "F").identify() # 'Bb major' + >>> Chord.from_tones("Bb", "D", "F").identify() + 'Bb major' You can also access the root and quality separately: -.. code-block:: python +.. code-block:: pycon - chord = Chord.from_name("Am7") - chord.root # - chord.quality # 'minor 7th' + >>> chord = Chord.from_name("Am7") + >>> chord.root + + >>> 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] diff --git a/docs/guide/fretboard.rst b/docs/guide/fretboard.rst index 33084ae..7140adc 100644 --- a/docs/guide/fretboard.rst +++ b/docs/guide/fretboard.rst @@ -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 `_ 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 `_ — 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 `_ @@ -93,12 +93,12 @@ The Bowed String Family The orchestral `string family `_ 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 `_ — 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 `_'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 `_ 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"), + ... ]) diff --git a/docs/guide/playback.rst b/docs/guide/playback.rst index 8623fa8..342fdbe 100644 --- a/docs/guide/playback.rst +++ b/docs/guide/playback.rst @@ -13,25 +13,25 @@ using basic `waveform `_ synthesis. Playing a Tone -------------- -.. code-block:: python +.. code-block:: pycon - from pytheory import Tone, play + >>> from pytheory import Tone, play - a4 = Tone.from_string("A4", system="western") - play(a4, t=1_000) # Play A440 for 1 second + >>> a4 = Tone.from_string("A4", system="western") + >>> play(a4, t=1_000) # Play A440 for 1 second Playing a Chord --------------- -.. code-block:: python +.. code-block:: pycon - from pytheory import Chord, play + >>> from pytheory import Chord, play - # From a chord name - play(Chord.from_name("Am7"), t=2_000) + >>> # From a chord name + >>> play(Chord.from_name("Am7"), t=2_000) - # From note names - play(Chord.from_tones("C", "E", "G"), t=2_000) + >>> # From note names + >>> play(Chord.from_tones("C", "E", "G"), t=2_000) Waveform Types -------------- @@ -52,26 +52,26 @@ integer multiples of the fundamental frequency. 1/n². Sounds softer and more mellow than sawtooth — somewhere between sine and sawtooth. Often described as "woody" or "hollow." -.. code-block:: python +.. code-block:: pycon - from pytheory import play, Synth, Tone + >>> from pytheory import play, Synth, Tone - tone = Tone.from_string("C4", system="western") + >>> tone = Tone.from_string("C4", system="western") - play(tone, synth=Synth.SINE) # Pure, clean - play(tone, synth=Synth.SAW) # Bright, buzzy - play(tone, synth=Synth.TRIANGLE) # Mellow, hollow + >>> play(tone, synth=Synth.SINE) # Pure, clean + >>> play(tone, synth=Synth.SAW) # Bright, buzzy + >>> play(tone, synth=Synth.TRIANGLE) # Mellow, hollow Temperaments ------------ Hear the difference between tuning systems: -.. code-block:: python +.. code-block:: pycon - play(tone, temperament="equal") # Modern standard (since ~1917) - play(tone, temperament="pythagorean") # Pure fifths, wolf intervals - play(tone, temperament="meantone") # Pure thirds, Renaissance sound + >>> play(tone, temperament="equal") # Modern standard (since ~1917) + >>> play(tone, temperament="pythagorean") # Pure fifths, wolf intervals + >>> play(tone, temperament="meantone") # Pure thirds, Renaissance sound Try playing a C major chord in each temperament — you'll hear subtle differences in the "color" of the major third. Equal temperament is @@ -84,16 +84,16 @@ Saving to WAV Render tones or chords to a WAV file instead of playing them live. This works even without speakers or PortAudio: -.. code-block:: python +.. code-block:: pycon - from pytheory import save, Chord, Tone, Synth + >>> from pytheory import save, Chord, Tone, Synth - # Save a single tone - save(Tone.from_string("A4"), "a440.wav", t=1_000) + >>> # Save a single tone + >>> save(Tone.from_string("A4"), "a440.wav", t=1_000) - # Save a chord - save(Chord.from_name("Am7"), "am7.wav", t=2_000) + >>> # Save a chord + >>> save(Chord.from_name("Am7"), "am7.wav", t=2_000) - # Choose waveform and temperament - save(Chord.from_name("C"), "c_triangle.wav", - synth=Synth.TRIANGLE, temperament="meantone", t=3_000) + >>> # Choose waveform and temperament + >>> save(Chord.from_name("C"), "c_triangle.wav", + ... synth=Synth.TRIANGLE, temperament="meantone", t=3_000) diff --git a/docs/guide/quickstart.rst b/docs/guide/quickstart.rst index 401af5b..faf2abe 100644 --- a/docs/guide/quickstart.rst +++ b/docs/guide/quickstart.rst @@ -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.from_midi(60) # + >>> Tone.from_frequency(440) + + >>> Tone.from_midi(60) + - # Tone arithmetic - c4 + 4 # — major third up - c4 + 7 # — perfect fifth up + >>> c4 + 4 + + >>> c4 + 7 + - # Interval between two tones - g4 = c4 + 7 - g4 - c4 # 7 semitones - c4.interval_to(g4) # 'perfect 5th' + >>> g4 = c4 + 7 + >>> g4 - c4 + 7 + >>> c4.interval_to(g4) + 'perfect 5th' - # Enharmonics - Tone.from_string("C#4", system="western").enharmonic # 'Db' + >>> Tone.from_string("C#4", system="western").enharmonic + 'Db' Scales ------ Build scales in any key and mode: -.. code-block:: python +.. code-block:: pycon - from pytheory import TonedScale + >>> from pytheory import TonedScale - c = TonedScale(tonic="C4") + >>> c = TonedScale(tonic="C4") - c["major"].note_names - # ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C'] + >>> c["major"].note_names + ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C'] - c["minor"].note_names - # ['C', 'D', 'D#', 'F', 'G', 'G#', 'A#', 'C'] + >>> c["minor"].note_names + ['C', 'D', 'D#', 'F', 'G', 'G#', 'A#', 'C'] - c["dorian"].note_names - # ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C'] + >>> c["dorian"].note_names + ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C'] - # Access scale degrees by name or numeral - major = c["major"] - major["tonic"] # C4 - major["dominant"] # G4 - major["V"] # G4 + >>> major = c["major"] + >>> major["tonic"] + C4 + >>> major["dominant"] + G4 + >>> major["V"] + G4 Keys and Chords --------------- @@ -78,41 +84,42 @@ Keys and Chords The :class:`~pytheory.scales.Key` class ties everything together — scales, chords, and progressions: -.. code-block:: python +.. code-block:: pycon - from pytheory import Key + >>> from pytheory import Key - key = Key("G", "major") - key.note_names # ['G', 'A', 'B', 'C', 'D', 'E', 'F#', 'G'] + >>> key = Key("G", "major") + >>> key.note_names + ['G', 'A', 'B', 'C', 'D', 'E', 'F#', 'G'] - # All diatonic triads - key.chords - # ['G major', 'A minor', 'B minor', 'C major', - # 'D major', 'E minor', 'F# diminished'] + >>> key.chords + ['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished'] - # Build progressions from Roman numerals - chords = key.progression("I", "V", "vi", "IV") - [c.identify() for c in chords] - # ['G major', 'D major', 'E minor', 'C major'] + >>> chords = key.progression("I", "V", "vi", "IV") + >>> [c.identify() for c in chords] + ['G major', 'D major', 'E minor', 'C major'] - # Detect the key from notes - Key.detect("C", "E", "G", "A", "D") # + >>> Key.detect("C", "E", "G", "A", "D") + Build chords directly: -.. code-block:: python +.. code-block:: pycon - from pytheory import Chord + >>> from pytheory import Chord - Chord.from_tones("C", "E", "G") # - Chord.from_name("Am7") # - Chord.from_intervals("G", 4, 7, 10) # + >>> Chord.from_tones("C", "E", "G") + + >>> Chord.from_name("Am7") + + >>> Chord.from_intervals("G", 4, 7, 10) + - # Identify any chord - Chord.from_tones("Bb", "D", "F").identify() # 'Bb major' + >>> Chord.from_tones("Bb", "D", "F").identify() + 'Bb major' - # Analyze in a key - Chord.from_name("G7").analyze("C") # 'V7' + >>> Chord.from_name("G7").analyze("C") + 'V7' Guitar Fingerings ----------------- @@ -124,7 +131,7 @@ Guitar Fingerings >>> fb = Fretboard.guitar() >>> fb.chord("C") - Fingering(e=0, B=1, G=0, D=2, A=3, E=0) + Fingering(e=0, B=1, G=0, D=2, A=3, E=x) >>> fb.chord("C")['A'] 3 @@ -132,31 +139,27 @@ Guitar Fingerings >>> fb.fingering(0, 0, 0, 2, 2, 0).identify() 'E minor' - >>> from pytheory import CHARTS - >>> print(CHARTS["western"]["Am"].tab(fretboard=fb)) - Am - E|--0-- + >>> print(fb.tab("Am")) + A minor + e|--0-- B|--1-- G|--2-- D|--2-- A|--0-- - E|--0-- + E|--x-- Audio Playback -------------- -.. code-block:: python +.. code-block:: pycon - from pytheory import Tone, Chord, play, save, Synth + >>> from pytheory import Tone, Chord, play, save, Synth - # Play a tone - play(Tone.from_string("A4"), t=1_000) + >>> play(Tone.from_string("A4"), t=1_000) - # Play a chord with a different waveform - play(Chord.from_name("Am7"), synth=Synth.TRIANGLE, t=2_000) + >>> play(Chord.from_name("Am7"), synth=Synth.TRIANGLE, t=2_000) - # Save to a WAV file - save(Chord.from_name("C"), "c_major.wav", t=2_000) + >>> save(Chord.from_name("C"), "c_major.wav", t=2_000) Command Line ------------ diff --git a/docs/guide/scales.rst b/docs/guide/scales.rst index 3c5daca..03e23da 100644 --- a/docs/guide/scales.rst +++ b/docs/guide/scales.rst @@ -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 `_ 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 `_ 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 `_ (ii) — minor with a raised 6th. Jazzy, soulful (So What, -Scarborough Fair):: +Scarborough Fair): - c["dorian"] # C D Eb F G A Bb C +.. code-block:: pycon + + >>> c["dorian"].note_names + ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C'] `Phrygian `_ (iii) — minor with a flat 2nd. Spanish, flamenco, dark -(White Rabbit):: +(White Rabbit): - c["phrygian"] # C Db Eb F G Ab Bb C +.. code-block:: pycon + + >>> c["phrygian"].note_names + ['C', 'C#', 'D#', 'F', 'G', 'G#', 'A#', 'C'] `Lydian `_ (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 `_ (V) — major with a flat 7th. Bluesy, rock, dominant -(Norwegian Wood, Sweet Home Alabama):: +(Norwegian Wood, Sweet Home Alabama): - c["mixolydian"] # C D E F G A Bb C +.. code-block:: pycon + + >>> c["mixolydian"].note_names + ['C', 'D', 'E', 'F', 'G', 'A', 'A#', 'C'] `Aeolian `_ (vi) — the natural minor scale. Sad, dark, introspective -(Stairway to Heaven, Losing My Religion):: +(Stairway to Heaven, Losing My Religion): - c["aeolian"] # C D Eb F G Ab Bb C +.. code-block:: pycon + + >>> c["aeolian"].note_names + ['C', 'D', 'D#', 'F', 'G', 'G#', 'A#', 'C'] `Locrian `_ (vii) — minor with flat 2nd and flat 5th. Unstable, rarely used as a home key (used in metal and jazz over diminished -chords):: +chords): - c["locrian"] # C Db Eb F Gb Ab Bb C +.. code-block:: pycon + + >>> c["locrian"].note_names + ['C', 'C#', 'D#', 'F', 'F#', 'G#', 'A#', 'C'] Scale Degrees ------------- @@ -137,32 +154,45 @@ Leading Tone VII One semitone below tonic — pulls upward Access degrees by index, Roman numeral, or name: -.. code-block:: python +.. code-block:: pycon - major = TonedScale(tonic="C4")["major"] - - major[0] # C4 (by index) - major["I"] # C4 (by Roman numeral) - major["tonic"] # C4 (by degree name) - - major["V"] # G4 (dominant) - major["dominant"] # G4 - - major[0:3] # (C4, D4, E4) — slicing works too + >>> major = TonedScale(tonic="C4")["major"] + >>> major[0] + C4 + >>> major["I"] + C4 + >>> major["tonic"] + C4 + >>> major["V"] + G4 + >>> major["dominant"] + G4 + >>> major[0:3] + (, , ) 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") # + >>> 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) + [, , , ] + >>> key.chords + ['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished'] + >>> key.seventh_chords + ['G major 7th', 'A minor 7th', 'B minor 7th', 'C major 7th', 'D dominant 7th', 'E minor 7th', 'F# half-diminished 7th'] + >>> Key.detect("C", "E", "G", "A", "D") + C major The 12-Bar Blues ~~~~~~~~~~~~~~~~ @@ -262,31 +290,26 @@ structure. In the key of A:: | D | D | A | A | | E | D | A | E | -.. code-block:: python +.. code-block:: pycon - from pytheory import TonedScale - - a = TonedScale(tonic="A4")["major"] - I = a.triad(0) # A major - IV = a.triad(3) # D major - V = a.triad(4) # E major - - # The 12-bar blues progression - blues_12 = [I, I, I, I, IV, IV, I, I, V, IV, I, V] + >>> from pytheory import TonedScale + >>> a = TonedScale(tonic="A4")["major"] + >>> I = a.triad(0) + >>> IV = a.triad(3) + >>> V = a.triad(4) + >>> blues_12 = [I, I, I, I, IV, IV, I, I, V, IV, I, V] Key Signatures ~~~~~~~~~~~~~~ The ``signature`` property tells you how many sharps or flats a key has: -.. code-block:: python +.. code-block:: pycon >>> Key("G", "major").signature {'sharps': 1, 'flats': 0, 'accidentals': ['F#']} - >>> Key("F", "major").signature - {'sharps': 0, 'flats': 1, 'accidentals': ['Bb']} - + {'sharps': 1, 'flats': 0, 'accidentals': ['A#']} >>> Key("C", "major").signature {'sharps': 0, 'flats': 0, 'accidentals': []} @@ -297,16 +320,14 @@ Two keys are **relative** if they share the same notes (C major and A minor). Two keys are `parallel `_ 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 - - + A minor >>> Key("A", "minor").relative - - + C major >>> Key("C", "major").parallel - + C minor Borrowed Chords ~~~~~~~~~~~~~~~ @@ -316,10 +337,10 @@ borrowing chords from the parallel key — is one of the most powerful tools in songwriting. The bVI and bVII chords (Ab and Bb in C major) are borrowed from C minor and appear constantly in rock and film music: -.. code-block:: python +.. code-block:: pycon >>> Key("C", "major").borrowed_chords - # Chords from C minor that aren't in C major + ['C minor', 'D diminished', 'D# major', 'F minor', 'G minor', 'G# major', 'A# major'] Secondary Dominants ~~~~~~~~~~~~~~~~~~~ @@ -328,15 +349,13 @@ A `secondary dominant `_ 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() [, , , , ...] @@ -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'] diff --git a/docs/guide/systems.rst b/docs/guide/systems.rst index 68455ef..63a0678 100644 --- a/docs/guide/systems.rst +++ b/docs/guide/systems.rst @@ -10,16 +10,16 @@ Western The standard 12-tone equal temperament system with major/minor scales and all seven modes. -.. code-block:: python +.. code-block:: pycon - from pytheory import TonedScale + >>> from pytheory import TonedScale - c = TonedScale(tonic="C4") - c["major"].note_names - # ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C'] + >>> c = TonedScale(tonic="C4") + >>> c["major"].note_names + ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C'] - c["dorian"].note_names - # ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C'] + >>> c["dorian"].note_names + ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C'] **Scales:** major, minor, harmonic minor, ionian, dorian, phrygian, lydian, mixolydian, aeolian, locrian, chromatic @@ -31,20 +31,20 @@ The Hindustani system uses **swaras** (Sa, Re, Ga, Ma, Pa, Dha, Ni) and organizes scales into `thaats `_ — the 10 parent scales from which `ragas `_ 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 >> from pytheory import TonedScale - do = TonedScale(tonic="Do4", system="arabic") + >>> do = TonedScale(tonic="Do4", system="arabic") - do["ajam"].note_names # = major scale - # ['Do', 'Re', 'Mi', 'Fa', 'Sol', 'La', 'Si', 'Do'] + >>> do["ajam"].note_names # = major scale + ['Do', 'Re', 'Mi', 'Fa', 'Sol', 'La', 'Si', 'Do'] - do["hijaz"].note_names # characteristic augmented 2nd - # ['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do'] + >>> do["hijaz"].note_names # characteristic augmented 2nd + ['Do', 'Reb', 'Mi', 'Fa', 'Sol', 'Solb', 'Sib', 'Do'] - do["nikriz"].note_names - # ['Do', 'Re', 'Mib', 'Fa#', 'Sol', 'La', 'Sib', 'Do'] + >>> do["nikriz"].note_names + ['Do', 'Re', 'Mib', 'Fa#', 'Sol', 'La', 'Sib', 'Do'] **Maqamat:** ajam, nahawand, kurd, hijaz, nikriz, bayati, rast, saba, sikah, jiharkah @@ -91,23 +91,23 @@ Japanese The Japanese system uses Western note names with traditional pentatonic and heptatonic scales from Japanese music. -.. code-block:: python +.. code-block:: pycon - from pytheory import TonedScale + >>> from pytheory import TonedScale - c = TonedScale(tonic="C4", system="japanese") + >>> c = TonedScale(tonic="C4", system="japanese") - c["hirajoshi"].note_names # most iconic Japanese scale - # ['C', 'D', 'D#', 'G', 'G#', 'C'] + >>> c["hirajoshi"].note_names # most iconic Japanese scale + ['C', 'D', 'D#', 'G', 'G#', 'C'] - c["in"].note_names # Miyako-bushi, used in koto music - # ['C', 'C#', 'F', 'G', 'G#', 'C'] + >>> c["in"].note_names # Miyako-bushi, used in koto music + ['C', 'C#', 'F', 'G', 'G#', 'C'] - c["yo"].note_names # folk music scale - # ['C', 'D', 'F', 'G', 'A#', 'C'] + >>> c["yo"].note_names # folk music scale + ['C', 'D', 'F', 'G', 'A#', 'C'] - c["ritsu"].note_names # gagaku court music (= Dorian) - # ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C'] + >>> c["ritsu"].note_names # gagaku court music (= Dorian) + ['C', 'D', 'D#', 'F', 'G', 'A', 'A#', 'C'] **Pentatonic scales:** hirajoshi, in, yo, iwato, kumoi, insen @@ -125,23 +125,23 @@ The `blues scale `_ adds the "`blue n minor pentatonic — this chromatic passing tone is the defining sound of the blues. -.. code-block:: python +.. code-block:: pycon - from pytheory import TonedScale + >>> from pytheory import TonedScale - c = TonedScale(tonic="C4", system="blues") + >>> c = TonedScale(tonic="C4", system="blues") - c["major pentatonic"].note_names # the "happy" pentatonic - # ['C', 'D', 'E', 'G', 'A', 'C'] + >>> c["major pentatonic"].note_names # the "happy" pentatonic + ['C', 'D', 'E', 'G', 'A', 'C'] - c["minor pentatonic"].note_names # the "sad" pentatonic - # ['C', 'D#', 'F', 'G', 'A#', 'C'] + >>> c["minor pentatonic"].note_names # the "sad" pentatonic + ['C', 'D#', 'F', 'G', 'A#', 'C'] - c["blues"].note_names # minor pentatonic + blue note - # ['C', 'D#', 'F', 'F#', 'G', 'A#', 'C'] + >>> c["blues"].note_names # minor pentatonic + blue note + ['C', 'D#', 'F', 'F#', 'G', 'A#', 'C'] - c["major blues"].note_names # major pentatonic + blue note - # ['C', 'D', 'D#', 'E', 'G', 'A', 'C'] + >>> c["major blues"].note_names # major pentatonic + blue note + ['C', 'D', 'D#', 'E', 'G', 'A', 'C'] **Pentatonic:** major pentatonic, minor pentatonic @@ -163,20 +163,20 @@ these are the closest 12-TET approximations. an ethereal, floating quality. `Pelog `_ 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 diff --git a/docs/guide/theory.rst b/docs/guide/theory.rst index 446d09b..12978f4 100644 --- a/docs/guide/theory.rst +++ b/docs/guide/theory.rst @@ -50,14 +50,13 @@ cycle almost closes. The tiny gap where it doesn't close perfectly is the `Pythagorean comma `_ — the reason we need `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 `_ uses 5 or 7 unequal @@ -183,17 +182,18 @@ is exactly this pattern. Every "Louie Louie" and every `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 `_. 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 `_ 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 --------------- diff --git a/docs/guide/tones.rst b/docs/guide/tones.rst index 47540cc..7fc5996 100644 --- a/docs/guide/tones.rst +++ b/docs/guide/tones.rst @@ -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.from_frequency(261.63) # + >>> Tone.from_frequency(440) + + >>> Tone.from_frequency(261.63) + - # From a MIDI note number - Tone.from_midi(60) # (middle C) - Tone.from_midi(69) # + >>> Tone.from_midi(60) + + >>> Tone.from_midi(69) + 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 - >>> c4 + 7 # Perfect fifth up + >>> c4 + 7 - >>> c4 + 12 # Octave up + >>> c4 + 12 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) - >>> c4.transpose(-2) # Two semitones down + >>> c4.transpose(-2) MIDI @@ -270,14 +264,13 @@ Every tone maps to a `MIDI note number `_ (0–127), the standard for communicating with synthesizers, DAWs, and digital instruments: -.. code-block:: python +.. code-block:: pycon >>> c4.midi - 60 # Middle C + 60 >>> Tone.from_string("A4", system="western").midi - 69 # Concert A + 69 - # Round-trip: MIDI → Tone → MIDI >>> Tone.from_midi(60).midi 60 @@ -286,7 +279,7 @@ Comparison and Sorting Tones can be compared and sorted by pitch frequency: -.. code-block:: python +.. code-block:: pycon >>> c4 < g4 True @@ -295,9 +288,9 @@ Tones can be compared and sorted by pitch frequency: Equality checks note name and octave: -.. code-block:: python +.. code-block:: pycon - >>> c4 == "C" # Compare with string (name only) + >>> c4 == "C" True >>> c4 == Tone(name="C", octave=4) True @@ -309,7 +302,7 @@ Every tone you hear is actually a composite of many frequencies. When a string vibrates, it doesn't just vibrate as a whole — it also vibrates in halves, thirds, quarters, and so on, producing the `harmonic series `_: -.. 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'] diff --git a/pytheory/charts.py b/pytheory/charts.py index 8fb2759..a6e9a08 100644 --- a/pytheory/charts.py +++ b/pytheory/charts.py @@ -107,6 +107,33 @@ class Fingering: """Identify the chord name from this fingering.""" return self.to_chord().identify() + def tab(self) -> str: + """Render this fingering as ASCII guitar tablature. + + Requires that the Fingering was created with a fretboard reference. + + Example:: + + >>> fb = Fretboard.guitar() + >>> print(fb.chord("C").tab()) + C + e|--0-- + B|--1-- + G|--0-- + D|--2-- + A|--3-- + E|--0-- + """ + if self._fretboard is None: + raise ValueError("Cannot render tab without a fretboard reference.") + name = self.identify() or "?" + lines = [name] + max_name = max(len(n) for n in self.string_names) + for sname, fret in zip(self.string_names, self.positions): + fret_str = "x" if fret is None else str(fret) + lines.append(f"{sname:>{max_name}}|--{fret_str}--") + return "\n".join(lines) + CHARTS = {} CHARTS["western"] = [] @@ -203,11 +230,12 @@ class NamedChord: fingering = [] for i, tone in enumerate(fretboard.tones): - fingering.append(find_fingerings(tone)) - - for i, finger in enumerate(fingering): - if finger == (): - fingering[i] = (-1,) + frets = find_fingerings(tone) + # Always allow muting as an option + if frets: + fingering.append((*frets, -1)) + else: + fingering.append((-1,)) return tuple(fingering) @@ -224,29 +252,76 @@ class NamedChord: def fingering(self, *, fretboard, multiple=False): def fingering_score(fingering): - def number_of_fingers(fingering): - zeros = 0 - for finger in fingering: - if finger == 0: - zeros += 1 - return len(fingering) - zeros + score = 0.0 + fretted = [f for f in fingering if f not in (0, -1)] + muted = sum(1 for f in fingering if f == -1) + sounding = len(fingering) - muted - def ascending(fingering): - fingering = [f for f in fingering if f != 0] + # Must have at least 2 sounding strings + if sounding < 2: + return -100.0 - return sorted(fingering) == fingering + # Check that all chord tones are present in the voicing + sounding_names = set() + for i, f in enumerate(fingering): + if f != -1: + sounding_names.add(fretboard.tones[i].add(f).name) + required = set(t.name for t in self.acceptable_tones) + missing = required - sounding_names + score -= len(missing) * 5.0 - ascending = int(ascending(fingering)) - finger_count = number_of_fingers(fingering) - return ascending + (1 / finger_count) + # Reward open strings + open_strings = sum(1 for f in fingering if f == 0) + score += open_strings * 2.0 + + # Penalize muted strings, but only mildly + score -= muted * 0.3 + + # Penalize fret span (hard to stretch) + if fretted: + span = max(fretted) - min(fretted) + score -= span * 2.0 + + # Penalize high fret positions (prefer open position) + if fretted: + score -= (sum(fretted) / len(fretted)) * 0.8 + + # Penalize many fingers needed + score -= len(fretted) * 0.3 + + # Reward root in bass — the lowest sounding string + for i in range(len(fingering) - 1, -1, -1): + f = fingering[i] + if f == -1: + continue + bass_tone = fretboard.tones[i].add(f) + if bass_tone.name == self.tone.name: + score += 4.0 + else: + # Penalize non-root bass notes + score -= 1.5 + break + + # Prefer muting from the bass side (contiguous muting) + # e.g. xx0232 is good, x0x232 is awkward + mute_from_bass = 0 + for i in range(len(fingering) - 1, -1, -1): + if fingering[i] == -1: + mute_from_bass += 1 + else: + break + interior_mutes = muted - mute_from_bass + score -= interior_mutes * 3.0 + + return score def gen(): fingerings = self.fingerings(fretboard=fretboard) - score_map = tuple(map(fingering_score, fingerings)) - max_score = max(score_map) + scored = [(fingering_score(f), f) for f in fingerings] + max_score = max(s for s, _ in scored) - for possible_fingering in fingerings: - if fingering_score(possible_fingering) == max_score: + for s, possible_fingering in scored: + if s == max_score: yield possible_fingering string_names = tuple(t.name for t in fretboard.tones) diff --git a/pytheory/chords.py b/pytheory/chords.py index 98a8688..2791dff 100644 --- a/pytheory/chords.py +++ b/pytheory/chords.py @@ -1270,6 +1270,46 @@ class Fretboard: from .charts import CHARTS return CHARTS[system][name].fingering(fretboard=self) + def tab(self, name: str, *, system: str = "western") -> str: + """Look up a chord by name and return its ASCII tablature. + + Args: + name: Chord name like ``"G"``, ``"Am7"``, ``"Bb"``. + system: Tonal system to use (default ``"western"``). + + Returns: + A multi-line string showing the chord as tablature. + + Example:: + + >>> fb = Fretboard.guitar() + >>> print(fb.tab("Am")) + A minor + e|--0-- + B|--1-- + G|--2-- + D|--2-- + A|--0-- + E|--0-- + """ + return self.chord(name, system=system).tab() + + def chart(self, *, system: str = "western") -> dict: + """Generate fingerings for every chord in the given system. + + Returns: + A dict mapping chord names to :class:`Fingering` objects. + + Example:: + + >>> fb = Fretboard.guitar() + >>> chart = fb.chart() + >>> chart["Am7"] + Fingering(e=0, B=1, G=0, D=2, A=0, E=0) + """ + from .charts import charts_for_fretboard, CHARTS + return charts_for_fretboard(chart=CHARTS[system], fretboard=self) + def fingering(self, *positions: int) -> "Fingering": """Apply fret positions to each string, returning a Fingering. diff --git a/test_pytheory.py b/test_pytheory.py index ea259db..c054413 100644 --- a/test_pytheory.py +++ b/test_pytheory.py @@ -2622,7 +2622,7 @@ def test_tension_empty(): def test_version(): import pytheory - assert pytheory.__version__ == "0.6.1" + assert pytheory.__version__ def test_all_exports(): @@ -3649,6 +3649,48 @@ def test_charts_muted_string(): assert fixed == (0, None, 2) +def test_fretboard_chord_method(): + """Fretboard.chord() looks up a chord by name.""" + fb = Fretboard.guitar() + f = fb.chord("G") + assert f.identify() == "G major" + assert len(f) == 6 + + +def test_fretboard_chord_system_kwarg(): + """Fretboard.chord() accepts a system keyword argument.""" + fb = Fretboard.guitar() + f = fb.chord("Am", system="western") + assert f.identify() == "A minor" + + +def test_fretboard_tab_method(): + """Fretboard.tab() returns ASCII tablature.""" + fb = Fretboard.guitar() + tab = fb.tab("C") + assert "C major" in tab + assert "e|" in tab + assert "E|" in tab + + +def test_fretboard_chart_method(): + """Fretboard.chart() generates all fingerings.""" + fb = Fretboard.guitar() + chart = fb.chart() + assert "C" in chart + assert "Am7" in chart + assert chart["C"].identify() == "C major" + + +def test_fingering_tab_method(): + """Fingering.tab() renders ASCII tablature.""" + fb = Fretboard.guitar() + f = fb.chord("Em") + tab = f.tab() + assert "E minor" in tab + assert "e|" in tab + + # ── Flat note support ───────────────────────────────────────────────────────── def test_flat_tone_from_string():