diff --git a/docs/guide/systems.rst b/docs/guide/systems.rst index acf2c11..43005ce 100644 --- a/docs/guide/systems.rst +++ b/docs/guide/systems.rst @@ -116,6 +116,84 @@ and heptatonic scales from Japanese music. **Heptatonic scales:** ritsu, ryo +Blues and Pentatonic +------------------- + +The blues system provides the scales foundational to blues, rock, jazz, +and folk music worldwide. Pentatonic scales (5 notes) are the oldest +known musical scales, found independently in cultures across every +continent. + +The **blues scale** adds the "blue note" (flat 5th / sharp 4th) to the +minor pentatonic — this chromatic passing tone is the defining sound +of the blues. + +.. code-block:: python + + from pytheory import TonedScale + from pytheory.systems import SYSTEMS + + c = TonedScale(tonic="C4", system=SYSTEMS["blues"]) + + 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["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'] + +**Pentatonic:** major pentatonic, minor pentatonic + +**Hexatonic:** blues, major blues + +**Heptatonic:** dominant (Mixolydian — the dominant 7th sound), +minor (Dorian — the jazz minor sound) + + +Javanese Gamelan +---------------- + +The gamelan system approximates the scales of the Javanese and Balinese +gamelan orchestra in 12-tone equal temperament. True gamelan tuning is +unique to each ensemble and does not conform to Western intonation — +these are the closest 12-TET approximations. + +**Slendro** is a roughly equal 5-tone division of the octave, producing +an ethereal, floating quality. **Pelog** is a 7-tone scale with unequal +intervals, typically performed using 5-note subsets called *pathet*. + +.. code-block:: python + + from pytheory import TonedScale + from pytheory.systems import SYSTEMS + + ji = TonedScale(tonic="ji4", system=SYSTEMS["gamelan"]) + + 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 nem"].note_names # pathet nem subset + # ['ji', 'ro-', 'lu', 'pat', 'mo', 'ji'] + +**Pentatonic:** slendro, pelog nem, pelog barang, pelog lima + +**Heptatonic:** pelog (full 7-tone) + +.. note:: + + Gamelan tone names follow Javanese numbering: ji (1), ro (2), + lu (3), pat (4), mo (5), nem (6), pi/barang (7). Suffixes + indicate microtonal variants approximated to the nearest semitone. + + Cross-System Comparison ----------------------- diff --git a/pytheory/_statics.py b/pytheory/_statics.py index 599e79f..4c1b1c6 100644 --- a/pytheory/_statics.py +++ b/pytheory/_statics.py @@ -68,6 +68,37 @@ TONES = { ("G",), ("G#", "Ab"), ], + # Blues/Pentatonic — Western names with blues and pentatonic scales. + "blues": [ + ("A",), + ("A#", "Bb"), + ("B",), + ("C",), + ("C#", "Db"), + ("D",), + ("D#", "Eb"), + ("E",), + ("F",), + ("F#", "Gb"), + ("G",), + ("G#", "Ab"), + ], + # Javanese gamelan — pelog approximation in 12-TET. + # True gamelan uses non-Western intonation; these are closest 12-TET fits. + "gamelan": [ + ("nem",), # A — 6 + ("pi",), # Bb — 7 (barang in some) + ("barang",), # B — 7 + ("ji",), # C — 1 + ("ro-",), # Db — 2b + ("ro",), # D — 2 + ("lu-",), # Eb — 3b + ("lu",), # E — 3 + ("pat",), # F — 4 + ("pat+",), # F# — 4# + ("mo",), # G — 5 + ("nem-",), # Ab — 6b + ], } DEGREES = { @@ -107,7 +138,25 @@ DEGREES = { ("san", ()), # 3rd ("shi", ()), # 4th ("go", ()), # 5th - ("roku", ()), # 6th (pentatonic scales skip some) + ("roku", ()), # 6th + ], + "blues": [ + ("tonic", ()), + ("supertonic", ()), + ("mediant", ()), + ("subdominant", ()), + ("dominant", ()), + ("submediant", ()), + ("subtonic", ()), + ], + "gamelan": [ + ("ji", ()), # 1 + ("ro", ()), # 2 + ("lu", ()), # 3 + ("pat", ()), # 4 + ("mo", ()), # 5 + ("nem", ()), # 6 + ("pi", ()), # 7 ], } @@ -254,6 +303,71 @@ JAPANESE_SCALES = { } } +# Blues and pentatonic scales — foundational to American music. +BLUES_SCALES = { + 12: { + "chromatic": (12, {}), + "pentatonic": [ + 5, + { + # Major pentatonic — C D E G A + "major pentatonic": {"intervals": (2, 2, 3, 2, 3)}, + # Minor pentatonic — C Eb F G Bb + "minor pentatonic": {"intervals": (3, 2, 2, 3, 2)}, + }, + ], + "hexatonic": [ + 6, + { + # Blues scale — C Eb F F# G Bb + "blues": {"intervals": (3, 2, 1, 1, 3, 2)}, + # Major blues — C D D# E G A + "major blues": {"intervals": (2, 1, 1, 3, 2, 3)}, + }, + ], + "heptatonic": [ + 7, + { + # Mixolydian (dominant blues sound) — C D E F G A Bb + "dominant": {"intervals": (2, 2, 1, 2, 2, 1, 2)}, + # Dorian (minor blues/jazz) — C D Eb F G A Bb + "minor": {"intervals": (2, 1, 2, 2, 2, 1, 2)}, + }, + ], + } +} + +# Javanese gamelan scales — 12-TET approximations. +# True gamelan tuning varies between ensembles and does not conform +# to equal temperament. These approximations capture the melodic +# character of the scales. +GAMELAN_SCALES = { + 12: { + "chromatic": (12, {}), + "pentatonic": [ + 5, + { + # Slendro — roughly equal 5-tone division of the octave + # Approximated as: C D F G Bb + "slendro": {"intervals": (2, 3, 2, 3, 2)}, + # Pelog pathet nem — C Db E F G (approx) + "pelog nem": {"intervals": (1, 3, 1, 2, 5)}, + # Pelog pathet barang — C Db E F# B (approx) + "pelog barang": {"intervals": (1, 3, 3, 4, 1)}, + # Pelog pathet lima — C Db E F Ab (approx) + "pelog lima": {"intervals": (1, 3, 1, 3, 4)}, + }, + ], + "heptatonic": [ + 7, + { + # Full pelog — all 7 tones: C Db E F G Ab B (approx) + "pelog": {"intervals": (1, 3, 1, 2, 1, 3, 1)}, + }, + ], + } +} + SYSTEMS = NotImplemented # Modes are rotations of the major scale pattern. diff --git a/pytheory/systems.py b/pytheory/systems.py index ba83615..2e42ae8 100644 --- a/pytheory/systems.py +++ b/pytheory/systems.py @@ -1,6 +1,7 @@ from ._statics import ( TEMPERAMENTS, TONES, DEGREES, SCALES, - INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES, SYSTEMS, + INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES, + BLUES_SCALES, GAMELAN_SCALES, SYSTEMS, ) @@ -134,4 +135,6 @@ SYSTEMS = { "indian": System(tone_names=TONES["indian"], degrees=DEGREES["indian"], scales=INDIAN_SCALES[12]), "arabic": System(tone_names=TONES["arabic"], degrees=DEGREES["arabic"], scales=ARABIC_SCALES[12]), "japanese": System(tone_names=TONES["japanese"], degrees=DEGREES["japanese"], scales=JAPANESE_SCALES[12]), + "blues": System(tone_names=TONES["blues"], degrees=DEGREES["blues"], scales=BLUES_SCALES[12]), + "gamelan": System(tone_names=TONES["gamelan"], degrees=DEGREES["gamelan"], scales=GAMELAN_SCALES[12]), } diff --git a/test_pytheory.py b/test_pytheory.py index c671b5d..5a659b9 100644 --- a/test_pytheory.py +++ b/test_pytheory.py @@ -2157,3 +2157,86 @@ def test_japanese_heptatonic_intervals_sum_to_12(): for name, scale in japanese.scales["heptatonic"].items(): total = sum(scale["intervals"]) assert total == 12, f"{name} intervals sum to {total}, not 12" + + +# ── Blues system ──────────────────────────────────────────────────────────── + +def test_blues_system_exists(): + assert "blues" in SYSTEMS + assert SYSTEMS["blues"].semitones == 12 + + +def test_blues_major_pentatonic(): + c = TonedScale(tonic="C4", system=SYSTEMS["blues"]) + s = c["major pentatonic"] + assert s.note_names == ["C", "D", "E", "G", "A", "C"] + + +def test_blues_minor_pentatonic(): + c = TonedScale(tonic="C4", system=SYSTEMS["blues"]) + s = c["minor pentatonic"] + assert s.note_names == ["C", "D#", "F", "G", "A#", "C"] + + +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 len(names) == 7 # 6 notes + octave + + +def test_blues_all_scales_available(): + c = TonedScale(tonic="C4", system=SYSTEMS["blues"]) + for scale in ["major pentatonic", "minor pentatonic", "blues", + "major blues", "dominant", "minor"]: + assert scale in c.scales, f"Missing scale: {scale}" + + +def test_blues_all_intervals_sum_to_12(): + blues = SYSTEMS["blues"] + for scale_type in blues.scales: + for name, scale in blues.scales[scale_type].items(): + total = sum(scale["intervals"]) + assert total == 12, f"{name} intervals sum to {total}, not 12" + + +# ── Gamelan system ────────────────────────────────────────────────────────── + +def test_gamelan_system_exists(): + assert "gamelan" in SYSTEMS + assert SYSTEMS["gamelan"].semitones == 12 + + +def test_gamelan_tones(): + gamelan = SYSTEMS["gamelan"] + names = [t.name for t in gamelan.tones] + assert "ji" in names + assert "ro" in names + assert "mo" in names + + +def test_gamelan_slendro(): + ji = TonedScale(tonic="ji4", system=SYSTEMS["gamelan"]) + s = ji["slendro"] + assert s.note_names == ["ji", "ro", "pat", "mo", "pi", "ji"] + + +def test_gamelan_pelog(): + ji = TonedScale(tonic="ji4", system=SYSTEMS["gamelan"]) + s = ji["pelog"] + assert len(s) == 8 # 7 notes + octave + + +def test_gamelan_all_scales_available(): + ji = TonedScale(tonic="ji4", system=SYSTEMS["gamelan"]) + for scale in ["slendro", "pelog nem", "pelog barang", "pelog lima", "pelog"]: + assert scale in ji.scales, f"Missing scale: {scale}" + + +def test_gamelan_all_intervals_sum_to_12(): + gamelan = SYSTEMS["gamelan"] + for scale_type in gamelan.scales: + for name, scale in gamelan.scales[scale_type].items(): + total = sum(scale["intervals"]) + assert total == 12, f"{name} intervals sum to {total}, not 12"