Add Blues/Pentatonic and Javanese Gamelan systems

Blues: major/minor pentatonic, blues scale, major blues, dominant,
  minor (Dorian). The foundational scales of blues, rock, and jazz.

Gamelan: slendro (5-tone equidistant), pelog (7-tone with 3 pathet
  subsets: nem, barang, lima). 12-TET approximations of Javanese
  gamelan tuning with traditional tone names (ji, ro, lu, pat, mo,
  nem, pi/barang).

Total systems: 6 (western, indian, arabic, japanese, blues, gamelan)
277 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 06:15:11 -04:00
parent 69ddb1eb64
commit 21cd99425b
4 changed files with 280 additions and 2 deletions
+78
View File
@@ -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
-----------------------
+115 -1
View File
@@ -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.
+4 -1
View File
@@ -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]),
}
+83
View File
@@ -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"