Add Arabic and Japanese systems, guitar tuning presets, logo, systems docs

New systems:
- Arabic (Maqam): 10 maqamat (ajam, hijaz, nahawand, nikriz, saba, etc.)
  with Arabic solfège tone names (Do, Re, Mi, Fa, Sol, La, Si)
- Japanese: 6 pentatonic scales (hirajoshi, in, yo, iwato, kumoi, insen)
  and 2 heptatonic scales (ritsu, ryo)

Fretboard improvements:
- Fretboard.guitar() now accepts tuning parameter
- Built-in tunings: standard, drop d, open g, open d, open e, open a,
  dadgad, half step down
- Custom tuning via tuple: Fretboard.guitar(("E4", "B3", ...))
- Fretboard.bass(five_string=True) for 5-string bass

Docs:
- Add Musical Systems guide page with all 4 systems
- Add logo to docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 06:06:17 -04:00
parent da08d30e8d
commit cceac40a88
8 changed files with 480 additions and 21 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

+1
View File
@@ -35,5 +35,6 @@ exclude_patterns = ["_build"]
html_theme = "alabaster"
html_title = "PyTheory"
html_logo = "_static/logo.png"
html_static_path = ["_static"]
html_extra_path = ["CNAME"]
+142
View File
@@ -0,0 +1,142 @@
Musical Systems
===============
PyTheory supports four musical systems, each with its own tone names
and scale patterns.
Western
-------
The standard 12-tone equal temperament system with major/minor scales
and all seven modes.
.. code-block:: python
from pytheory import TonedScale
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']
**Scales:** major, minor, harmonic minor, ionian, dorian, phrygian,
lydian, mixolydian, aeolian, locrian, chromatic
Indian Classical (Hindustani)
-----------------------------
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
from pytheory import TonedScale
from pytheory.systems import SYSTEMS
sa = TonedScale(tonic="Sa4", system=SYSTEMS["indian"])
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["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
**Swara notation:**
- Uppercase = shuddha (natural): Sa, Re, Ga, Ma, Pa, Dha, Ni
- ``komal`` prefix = flat: komal Re, komal Ga, komal Dha, komal Ni
- ``tivra`` prefix = sharp: tivra Ma
Arabic Maqam
------------
The Arabic system uses **solfège-based names** (Do, Re, Mi, Fa, Sol, La, Si)
and organizes scales into **maqamat** (plural of maqam).
.. note::
True maqam music uses quarter-tones that cannot be represented in
12-tone equal temperament. These scales are the closest 12-TET
approximations.
.. code-block:: python
from pytheory import TonedScale
from pytheory.systems import SYSTEMS
do = TonedScale(tonic="Do4", system=SYSTEMS["arabic"])
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["nikriz"].note_names
# ['Do', 'Re', 'Mib', 'Fa#', 'Sol', 'La', 'Sib', 'Do']
**Maqamat:** ajam, nahawand, kurd, hijaz, nikriz, bayati, rast, saba,
sikah, jiharkah
Japanese
--------
The Japanese system uses Western note names with traditional pentatonic
and heptatonic scales from Japanese music.
.. code-block:: python
from pytheory import TonedScale
from pytheory.systems import SYSTEMS
c = TonedScale(tonic="C4", system=SYSTEMS["japanese"])
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["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']
**Pentatonic scales:** hirajoshi, in, yo, iwato, kumoi, insen
**Heptatonic scales:** ritsu, ryo
Cross-System Comparison
-----------------------
Since all systems use 12-tone equal temperament, equivalent scales
produce the same pitches:
.. code-block:: python
from pytheory import TonedScale, Tone
from pytheory.systems import SYSTEMS
# These are all the same scale with different names
western = TonedScale(tonic="C4")["major"]
indian = TonedScale(tonic="Sa4", system=SYSTEMS["indian"])["bilawal"]
arabic = TonedScale(tonic="Do4", system=SYSTEMS["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")
c4.frequency # 261.63
sa4.frequency # 261.63
do4.frequency # 261.63
+1
View File
@@ -31,6 +31,7 @@ Work with tones, scales, chords, and fretboards using a clean, Pythonic API.
guide/scales
guide/chords
guide/fretboard
guide/systems
guide/playback
.. toctree::
+122
View File
@@ -38,6 +38,36 @@ TONES = {
("Pa",), # G — pancham
("komal Dha",), # Ab — komal dhaivat
],
# Arabic maqam system — Arabic solfège names.
"arabic": [
("La",), # A
("Sib",), # Bb — Si bemol
("Si",), # B
("Do",), # C
("Reb",), # Db — Re bemol
("Re",), # D
("Mib",), # Eb — Mi bemol
("Mi",), # E
("Fa",), # F
("Fa#",), # F#
("Sol",), # G
("Solb",), # Ab — Sol bemol
],
# Japanese system — uses Western names; scales are the unique part.
"japanese": [
("A",),
("A#", "Bb"),
("B",),
("C",),
("C#", "Db"),
("D",),
("D#", "Eb"),
("E",),
("F",),
("F#", "Gb"),
("G",),
("G#", "Ab"),
],
}
DEGREES = {
@@ -61,6 +91,24 @@ DEGREES = {
("nishad", ()), # Ni — 7th
("saptak", ()), # Sa — octave
],
"arabic": [
("qarar", ()), # 1st — root
("nawa", ()), # 2nd
("thalth", ()), # 3rd
("arba", ()), # 4th
("khamis", ()), # 5th
("sadis", ()), # 6th
("sabi", ()), # 7th
("jawab", ()), # octave
],
"japanese": [
("ichi", ()), # 1st
("ni", ()), # 2nd
("san", ()), # 3rd
("shi", ()), # 4th
("go", ()), # 5th
("roku", ()), # 6th (pentatonic scales skip some)
],
}
SCALES = {
@@ -132,6 +180,80 @@ INDIAN_SCALES = {
}
}
# Arabic maqam scales (12-TET approximations).
# True maqam uses quarter-tones; these are the closest 12-tone equivalents.
ARABIC_SCALES = {
12: {
"chromatic": (12, {}),
"maqam": [
7,
{
# Ajam = Western major
"ajam": {"intervals": (2, 2, 1, 2, 2, 2, 1)},
# Nahawand = Western harmonic minor
"nahawand": {"intervals": (2, 1, 2, 2, 1, 3, 1)},
# Kurd = Western Phrygian
"kurd": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
# Hijaz — augmented 2nd between 2nd and 3rd degrees
"hijaz": {"intervals": (1, 3, 1, 2, 1, 2, 2)},
# Nikriz — augmented 2nd between 3rd and 4th
"nikriz": {"intervals": (2, 1, 3, 1, 2, 1, 2)},
# Bayati (12-TET approx) — true bayati has quarter-flat 2nd
"bayati": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
# Rast (12-TET approx) — true rast has quarter-flat 3rd and 7th
"rast": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
# Saba (12-TET approx) — true saba has quarter-flat 2nd
"saba": {"intervals": (1, 2, 1, 3, 1, 2, 2)},
# Sikah (12-TET approx) — true sikah starts on quarter-flat
"sikah": {"intervals": (1, 2, 2, 2, 1, 2, 2)},
# Jiharkah
"jiharkah": {"intervals": (2, 2, 1, 2, 2, 1, 2)},
},
],
}
}
# Japanese pentatonic scales.
JAPANESE_SCALES = {
12: {
"chromatic": (12, {}),
"pentatonic": [
5,
{
# Hirajoshi — the most well-known Japanese scale
# C D Eb G Ab
"hirajoshi": {"intervals": (2, 1, 4, 1, 4)},
# In (Miyako-bushi) — used in koto music
# C Db F G Ab
"in": {"intervals": (1, 4, 2, 1, 4)},
# Yo — folk music scale
# C D F G Bb
"yo": {"intervals": (2, 3, 2, 3, 2)},
# Iwato — dark, dissonant pentatonic
# C Db F Gb Bb
"iwato": {"intervals": (1, 4, 1, 4, 2)},
# Kumoi — similar to minor pentatonic
# C D Eb G A
"kumoi": {"intervals": (2, 1, 4, 2, 3)},
# Insen — modern Japanese scale
# C Db F G Bb
"insen": {"intervals": (1, 4, 2, 3, 2)},
},
],
"heptatonic": [
7,
{
# Ritsu — gagaku court music scale
# C D Eb F G A Bb (= Dorian)
"ritsu": {"intervals": (2, 1, 2, 2, 2, 1, 2)},
# Ryo — gagaku court music scale
# C D E F# G A B (= Lydian)
"ryo": {"intervals": (2, 2, 2, 1, 2, 2, 1)},
},
],
}
}
SYSTEMS = NotImplemented
# Modes are rotations of the major scale pattern.
+34 -20
View File
@@ -104,29 +104,43 @@ class Fretboard:
def __len__(self):
return len(self.tones)
@classmethod
def guitar(cls):
"""Standard guitar tuning (E4 B3 G3 D3 A2 E2)."""
from .tones import Tone
return cls(tones=[
Tone.from_string("E4", system="western"),
Tone.from_string("B3", system="western"),
Tone.from_string("G3", system="western"),
Tone.from_string("D3", system="western"),
Tone.from_string("A2", system="western"),
Tone.from_string("E2", system="western"),
])
TUNINGS = {
"standard": ("E4", "B3", "G3", "D3", "A2", "E2"),
"drop d": ("E4", "B3", "G3", "D3", "A2", "D2"),
"open g": ("D4", "B3", "G3", "D3", "G2", "D2"),
"open d": ("D4", "A3", "F#3", "D3", "A2", "D2"),
"open e": ("E4", "B3", "G#3", "E3", "B2", "E2"),
"open a": ("E4", "C#4", "A3", "E3", "A2", "E2"),
"dadgad": ("D4", "A3", "G3", "D3", "A2", "D2"),
"half step down": ("D#4", "A#3", "F#3", "C#3", "G#2", "D#2"),
}
@classmethod
def bass(cls):
"""Standard bass guitar tuning (G2 D2 A1 E1)."""
def guitar(cls, tuning="standard"):
"""Guitar with the given tuning.
Args:
tuning: Tuning name or tuple of tone strings (high to low).
Built-in tunings: standard, drop d, open g, open d,
open e, open a, dadgad, half step down.
"""
from .tones import Tone
return cls(tones=[
Tone.from_string("G2", system="western"),
Tone.from_string("D2", system="western"),
Tone.from_string("A1", system="western"),
Tone.from_string("E1", system="western"),
])
if isinstance(tuning, str):
tuning = cls.TUNINGS[tuning]
return cls(tones=[Tone.from_string(t, system="western") for t in tuning])
@classmethod
def bass(cls, five_string=False):
"""Standard bass guitar tuning.
Args:
five_string: If True, adds a low B string (B0).
"""
from .tones import Tone
strings = ["G2", "D2", "A1", "E1"]
if five_string:
strings.append("B0")
return cls(tones=[Tone.from_string(t, system="western") for t in strings])
@classmethod
def ukulele(cls):
+6 -1
View File
@@ -1,4 +1,7 @@
from ._statics import TEMPERAMENTS, TONES, DEGREES, SCALES, INDIAN_SCALES, SYSTEMS
from ._statics import (
TEMPERAMENTS, TONES, DEGREES, SCALES,
INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES, SYSTEMS,
)
class System:
@@ -129,4 +132,6 @@ class System:
SYSTEMS = {
"western": System(tone_names=TONES["western"], degrees=DEGREES["western"]),
"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]),
}
+174
View File
@@ -1725,6 +1725,37 @@ def test_fretboard_ukulele_fingerings():
assert len(fingering) == 4
def test_fretboard_guitar_drop_d():
fb = Fretboard.guitar("drop d")
assert len(fb) == 6
assert fb.tones[-1].name == "D"
assert fb.tones[-1].octave == 2
def test_fretboard_guitar_open_g():
fb = Fretboard.guitar("open g")
assert len(fb) == 6
assert fb.tones[0].name == "D"
def test_fretboard_guitar_custom_tuple():
fb = Fretboard.guitar(("E4", "B3", "G3", "D3", "A2", "D2"))
assert len(fb) == 6
assert fb.tones[-1].name == "D"
def test_fretboard_bass_five_string():
fb = Fretboard.bass(five_string=True)
assert len(fb) == 5
assert fb.tones[-1].name == "B"
def test_fretboard_tunings_dict():
for name in Fretboard.TUNINGS:
fb = Fretboard.guitar(name)
assert len(fb) == 6, f"Tuning {name} should have 6 strings"
# ── Ergonomic integration tests ─────────────────────────────────────────────
def test_ergonomic_workflow():
@@ -1910,3 +1941,146 @@ def test_indian_scale_degree_access():
assert bilawal[4].name == "Pa"
assert bilawal["I"].name == "Sa"
assert bilawal["V"].name == "Pa"
# ── Arabic system ───────────────────────────────────────────────────────────
def test_arabic_system_exists():
assert "arabic" in SYSTEMS
assert SYSTEMS["arabic"].semitones == 12
def test_arabic_tones():
arabic = SYSTEMS["arabic"]
names = [t.name for t in arabic.tones]
assert "Do" in names
assert "Re" in names
assert "Sol" in names
def test_arabic_do_pitch():
"""Do4 should equal C4 = 261.63 Hz."""
do = Tone.from_string("Do4", system="arabic")
assert abs(do.frequency - 261.63) < 0.01
def test_arabic_ajam_maqam():
"""Ajam = major scale."""
do = TonedScale(tonic="Do4", system=SYSTEMS["arabic"])
ajam = do["ajam"]
names = [t.name for t in ajam]
assert names == ["Do", "Re", "Mi", "Fa", "Sol", "La", "Si", "Do"]
def test_arabic_hijaz_maqam():
"""Hijaz has augmented 2nd between 2nd and 3rd degrees."""
do = TonedScale(tonic="Do4", system=SYSTEMS["arabic"])
hijaz = do["hijaz"]
names = [t.name for t in hijaz]
assert names[0] == "Do"
assert names[1] == "Reb" # flat 2nd
assert names[2] == "Mi" # natural 3rd (augmented 2nd interval)
def test_arabic_all_maqamat_available():
do = TonedScale(tonic="Do4", system=SYSTEMS["arabic"])
for maqam in ["ajam", "nahawand", "kurd", "hijaz", "nikriz",
"bayati", "rast", "saba", "sikah", "jiharkah"]:
assert maqam in do.scales, f"Missing maqam: {maqam}"
def test_arabic_all_maqam_intervals_sum_to_12():
arabic = SYSTEMS["arabic"]
for name, scale in arabic.scales["maqam"].items():
total = sum(scale["intervals"])
assert total == 12, f"{name} intervals sum to {total}, not 12"
def test_arabic_ajam_equals_western_major():
arabic = SYSTEMS["arabic"]
western = SYSTEMS["western"]
ajam = arabic.scales["maqam"]["ajam"]["intervals"]
major = western.scales["heptatonic"]["major"]["intervals"]
assert ajam == major
def test_arabic_tone_arithmetic():
do = Tone.from_string("Do4", system="arabic")
assert (do + 2).name == "Re"
assert (do + 4).name == "Mi"
assert (do + 7).name == "Sol"
# ── Japanese system ─────────────────────────────────────────────────────────
def test_japanese_system_exists():
assert "japanese" in SYSTEMS
assert SYSTEMS["japanese"].semitones == 12
def test_japanese_hirajoshi():
"""Hirajoshi: C D Eb G Ab."""
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"]
def test_japanese_in_scale():
"""In (Miyako-bushi): C Db F G Ab."""
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"]
def test_japanese_yo_scale():
"""Yo: C D F G Bb."""
c = TonedScale(tonic="C4", system=SYSTEMS["japanese"])
s = c["yo"]
names = [t.name for t in s]
assert names == ["C", "D", "F", "G", "A#", "C"]
def test_japanese_iwato():
"""Iwato: C Db F Gb Bb."""
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"]
def test_japanese_kumoi():
"""Kumoi: C D Eb G A."""
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"]
def test_japanese_ritsu():
"""Ritsu (gagaku): C D Eb F G A Bb = Dorian."""
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"]
def test_japanese_all_scales_available():
c = TonedScale(tonic="C4", system=SYSTEMS["japanese"])
for scale in ["hirajoshi", "in", "yo", "iwato", "kumoi", "insen", "ritsu", "ryo"]:
assert scale in c.scales, f"Missing scale: {scale}"
def test_japanese_pentatonic_intervals_sum_to_12():
japanese = SYSTEMS["japanese"]
for name, scale in japanese.scales["pentatonic"].items():
total = sum(scale["intervals"])
assert total == 12, f"{name} intervals sum to {total}, not 12"
def test_japanese_heptatonic_intervals_sum_to_12():
japanese = SYSTEMS["japanese"]
for name, scale in japanese.scales["heptatonic"].items():
total = sum(scale["intervals"])
assert total == 12, f"{name} intervals sum to {total}, not 12"