diff --git a/pytheory/_statics.py b/pytheory/_statics.py index 41baa78..872f50f 100644 --- a/pytheory/_statics.py +++ b/pytheory/_statics.py @@ -295,8 +295,51 @@ DEGREES_SHRUTI = [ ("shadja", ()), # Sa (octave) ] +# 22-shruti frequency ratios — 5-limit just intonation. +# These are the REAL shruti intervals, NOT 22-TET approximations. +# Based on the traditional Pythagorean/harmonic ratios from Indian +# musicological treatises (Natya Shastra, Sangita Ratnakara). +# +# Ordered from Dha (A=1.0) to match our system indexing. +# Sa is at index 5 (ratio ≈ 6/5 from Dha). +from fractions import Fraction +_SHRUTI_RATIOS_FROM_SA = [ + Fraction(1, 1), # 0: Sa — 1/1 + Fraction(256, 243), # 1: atikomal Re — Pythagorean limma + Fraction(16, 15), # 2: komal Re — JI minor second + Fraction(10, 9), # 3: shuddha Re — minor whole tone + Fraction(9, 8), # 4: Re — major whole tone + Fraction(32, 27), # 5: atikomal Ga — Pythagorean minor 3rd + Fraction(6, 5), # 6: komal Ga — JI minor 3rd + Fraction(5, 4), # 7: Ga — JI major 3rd + Fraction(81, 64), # 8: tivra Ga — Pythagorean major 3rd + Fraction(4, 3), # 9: Ma — perfect 4th + Fraction(27, 20), # 10: ekashruti Ma + Fraction(45, 32), # 11: tivra Ma — augmented 4th + Fraction(729, 512), # 12: atitivra Ma — Pythagorean tritone + Fraction(3, 2), # 13: Pa — perfect 5th + Fraction(128, 81), # 14: atikomal Dha — Pythagorean minor 6th + Fraction(8, 5), # 15: komal Dha — JI minor 6th + Fraction(5, 3), # 16: shuddha Dha + Fraction(27, 16), # 17: Dha — Pythagorean major 6th + Fraction(16, 9), # 18: komal Ni — Pythagorean minor 7th + Fraction(9, 5), # 19: shuddha Ni — JI minor 7th + Fraction(15, 8), # 20: Ni — JI major 7th + Fraction(243, 128), # 21: tivra Ni — Pythagorean major 7th +] + +# Rotate to start from Dha (index 17 in the Sa-based list above). +# Dha = 27/16 from Sa. We divide all ratios by 27/16 and wrap. +_dha_ratio = _SHRUTI_RATIOS_FROM_SA[17] +SHRUTI_RATIOS = [] +for i in range(22): + sa_idx = (i + 17) % 22 # rotate: Dha=0, komalNi=1, ..., Sa=5, ... + r = _SHRUTI_RATIOS_FROM_SA[sa_idx] / _dha_ratio + if r < 1: + r *= 2 # wrap into the same octave + SHRUTI_RATIOS.append(float(r)) + # 22-shruti thaat scales with proper microtonal intervals. -# Each interval is counted in shrutis (22-TET steps). # Compare to the 12-TET approximations in INDIAN_SCALES which lose # the distinction between 2-shruti and 3-shruti steps. SHRUTI_SCALES = { diff --git a/pytheory/systems.py b/pytheory/systems.py index 67587c6..23f4d24 100644 --- a/pytheory/systems.py +++ b/pytheory/systems.py @@ -2,7 +2,7 @@ from ._statics import ( TEMPERAMENTS, TONES, DEGREES, SCALES, INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES, BLUES_SCALES, GAMELAN_SCALES, SYSTEMS, - TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES, + TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES, SHRUTI_RATIOS, TONES_ARABIC_24, DEGREES_ARABIC_24, ARABIC_24_SCALES, TONES_SLENDRO, DEGREES_SLENDRO, SLENDRO_SCALES, TONES_PELOG, DEGREES_PELOG, PELOG_SCALES, @@ -14,7 +14,7 @@ from ._statics import ( class System: def __init__(self, *, tone_names, degrees, scales=None, c_index=None, - period=2.0): + period=2.0, ratios=None): self.tone_names = tone_names self.degrees = degrees @@ -25,6 +25,11 @@ class System: # 3.0 for Bohlen-Pierce (tritave). self.period = period + # Custom frequency ratios: if set, overrides equal temperament. + # A list of N floats (one per tone), each relative to the first + # tone (1.0). For example, just intonation shruti ratios. + self.ratios = ratios + # c_index: the index of the "reference C" in the tone list. # For octave arithmetic — scientific pitch changes octave at C. # Default 3 for 12-TET western (A=0, A#=1, B=2, C=3). @@ -214,6 +219,17 @@ class System: # descending goes in meta? return {"intervals": scale, "hemitonic": hemitonic, "meta": {}} + def tone(self, name, octave=4): + """Create a Tone in this system. Shorthand for ``Tone(name, octave=octave, system=self)``. + + Example:: + + >>> edo19 = TET(19) + >>> edo19.tone(5, octave=4).frequency + """ + from . import Tone + return Tone(name, octave=octave, system=self) + def __repr__(self): return f"" @@ -352,7 +368,7 @@ SYSTEMS = { "31-tet": TET(31, names=_31TET_NAMES), # Microtonal systems with proper intervals (not 12-TET approximations) "shruti": System(tone_names=TONES_SHRUTI, degrees=DEGREES_SHRUTI, - scales=SHRUTI_SCALES, c_index=5), + scales=SHRUTI_SCALES, c_index=5, ratios=SHRUTI_RATIOS), "maqam": System(tone_names=TONES_ARABIC_24, degrees=DEGREES_ARABIC_24, scales=ARABIC_24_SCALES, c_index=5), "slendro": System(tone_names=TONES_SLENDRO, degrees=DEGREES_SLENDRO, diff --git a/pytheory/tones.py b/pytheory/tones.py index 0a914ff..5a153fb 100644 --- a/pytheory/tones.py +++ b/pytheory/tones.py @@ -26,7 +26,7 @@ class Tone: def __init__( self, - name: str, + name, *, alt_names: Optional[list[str]] = None, octave: Optional[int] = None, @@ -36,8 +36,10 @@ class Tone: """Initialize a Tone with a name, optional octave, and musical system. Args: - name: The note name (e.g. ``"C"``, ``"C#4"``). If the name - contains a digit, it is parsed as the octave. + name: The note name as a string (``"C"``, ``"C#4"``) or an int + for numbered systems (``0``, ``11``). Ints are converted to + strings and wrapped to the system's range (e.g. 22 in a + 22-tone system becomes 0 at octave+1). alt_names: Alternate spellings for this tone (e.g. enharmonics). octave: The octave number. Overrides any octave parsed from *name*. system: The tuning system, either as a string key (``"western"``) @@ -46,6 +48,23 @@ class Tone: if alt_names is None: alt_names = [] + # Int tone names: wrap to system range, adjust octave + if isinstance(name, int): + if isinstance(system, str): + from .systems import SYSTEMS + _sys = SYSTEMS[system] + else: + _sys = system + n_tones = len(_sys.tone_names) + if name < 0 or name >= n_tones: + extra_octaves = name // n_tones + name = name % n_tones + if octave is None: + octave = 4 + extra_octaves + else: + octave += extra_octaves + name = str(name) + if isinstance(name, str): # Normalize unicode music symbols to ASCII equivalents name = (name @@ -762,9 +781,12 @@ class Tone: period = getattr(self.system, 'period', 2.0) c_idx = getattr(self.system, 'c_index', C_INDEX) - if period != 2.0 and temperament == "equal": - # Non-octave period (e.g. Bohlen-Pierce tritave=3.0): - # generate ratios as period^(n/tones) instead of 2^(n/tones) + # Custom ratios override temperament (e.g. shruti just ratios) + custom_ratios = getattr(self.system, 'ratios', None) + if custom_ratios is not None: + pitch_scale = list(custom_ratios) + [period] + elif period != 2.0 and temperament == "equal": + # Non-octave period (e.g. Bohlen-Pierce tritave=3.0) import sympy pitch_scale = [period ** sympy.Rational(i, tones) for i in range(tones + 1)] else: