Int tone names, wrapping, System.tone(), proper shruti JI ratios

- Tone(0, system=edo22) works alongside Tone("0", ...)
- Tone(22, system=edo22) wraps to tone 0, octave+1
- Tone(-1) wraps to last tone, octave-1
- System.tone(name, octave) convenience method
- Shruti system now uses 5-limit just intonation ratios instead
  of 22-TET approximation. Based on Pythagorean/harmonic ratios
  from traditional Indian musicology. Pa is a pure 3/2, Ga is a
  pure 5/4.
- System.ratios attribute overrides equal temperament when set

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 10:54:28 -04:00
parent aa405702a9
commit 3acde86028
3 changed files with 91 additions and 10 deletions
+44 -1
View File
@@ -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 = {
+19 -3
View File
@@ -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"<System semitones={self.semitones!r}>"
@@ -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,
+28 -6
View File
@@ -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: