Source code for pytheory.systems

from ._statics import (
    TEMPERAMENTS, TONES, DEGREES, SCALES,
    INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES,
    BLUES_SCALES, GAMELAN_SCALES, SYSTEMS,
    TONES_SHRUTI, DEGREES_SHRUTI, SHRUTI_SCALES, SHRUTI_RATIOS,
    TONES_ARABIC_24, DEGREES_ARABIC_24, ARABIC_24_SCALES, MAQAM_RATIOS,
    TONES_SLENDRO, DEGREES_SLENDRO, SLENDRO_SCALES,
    TONES_PELOG, DEGREES_PELOG, PELOG_SCALES,
    TONES_THAI, DEGREES_THAI, THAI_SCALES,
    TONES_TURKISH, DEGREES_TURKISH, TURKISH_SCALES,
    TONES_CARNATIC, DEGREES_CARNATIC, CARNATIC_SCALES,
)


[docs] class System:
[docs] def __init__(self, *, tone_names, degrees, scales=None, c_index=None, period=2.0, ratios=None): self.tone_names = tone_names self.degrees = degrees self._scales = scales # Period: the frequency ratio of one "octave" in this system. # 2.0 for standard octave-based systems. # 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). # For non-12-TET systems, this is the index of the tone nearest C, # or 0 if no C equivalent exists. if c_index is not None: self.c_index = c_index else: # Try to find C in the tone names, fall back to 0 self.c_index = 0 for i, names in enumerate(tone_names): if "C" in names: self.c_index = i break if scales is None: n = self.semitones if n in SCALES: self._scales = SCALES[n] else: # Generate chromatic scale for unknown sizes self._scales = { "chromatic": (n, {}), }
@property def semitones(self): return len(self.tone_names) @property def tones(self): from . import Tone return tuple([Tone.from_tuple(tone) for tone in self.tone_names])
[docs] def resolve_name(self, name: str) -> str | None: """Resolve a note name (including flats, double sharps/flats) to the canonical name. Handles enharmonic equivalents: - Standard names and their alternates (e.g. Bb, C#) - Double sharps (C## = D, F## = G) - Double flats (Dbb = C, Ebb = D) Returns the primary name if found, or None if not recognized. """ # Direct lookup first for names in self.tone_names: if name in names: return names[0] # Handle double sharps (e.g. C## → D, F## → G) if name.endswith('##') and len(name) >= 3: base = name[:-2] base_idx = self._name_to_index(base) if base_idx is not None: resolved_idx = (base_idx + 2) % len(self.tone_names) return self.tone_names[resolved_idx][0] # Handle double flats (e.g. Dbb → C, Ebb → D) if name.endswith('bb') and len(name) >= 3 and name[0] != 'b': base = name[:-2] base_idx = self._name_to_index(base) if base_idx is not None: resolved_idx = (base_idx - 2) % len(self.tone_names) return self.tone_names[resolved_idx][0] # Handle single sharps/flats on natural notes (e.g. Cb → B, E# → F) if len(name) == 2: base = name[0] modifier = name[1] base_idx = self._name_to_index(base) if base_idx is not None: if modifier == '#': resolved_idx = (base_idx + 1) % len(self.tone_names) return self.tone_names[resolved_idx][0] elif modifier == 'b': resolved_idx = (base_idx - 1) % len(self.tone_names) return self.tone_names[resolved_idx][0] return None
def _name_to_index(self, name: str) -> int | None: """Return the index of a tone name, or None if not found.""" for i, names in enumerate(self.tone_names): if name in names: return i return None @property def scales(self): scales = {} for (scale_type, scale_properties) in self._scales.items(): scales[scale_type] = {} tones = scale_properties[0] new_scales = scale_properties[1] if not new_scales: new_scales = {scale_type: {}} for scale in new_scales.items(): scale_name = scale[0] scales[scale_type][scale_name] = self.generate_scale( tones=tones, semitones=self.semitones, **scale[1] ) return scales @property def modes(self): def gen(): for i, degree in enumerate(self.degrees): for mode in degree[1]: yield {"degree": (i + 1), "mode": mode} return [g for g in gen()]
[docs] @staticmethod def generate_scale( *, tones=7, semitones=12, intervals=None, major=False, minor=False, hemitonic=False, # Contains semitones. harmonic=False, melodic=False, offset=None, ): """Generates the primary scale for a given number of semitones/tones.""" # Direct interval pattern — bypass generation logic. if intervals is not None: scale = list(intervals) if offset: scale = scale[offset:] + scale[:offset] return {"intervals": scale, "hemitonic": 1 in scale, "meta": {}} # Sanity check. if major and minor: raise ValueError("Scale cannot be both major and minor. Choose one.") def gen(tones, semitones, major, minor, harmonic, melodic, hemitonic): if major or minor: hemitonic = True # Assume chromatic scale, if neither major nor minor. if not (major or minor) and not hemitonic: for i in range(tones): yield 1 else: if hemitonic: if major: pattern = (2, 2, 1, 2, 2, 2, 1) elif minor: pattern = (2, 1, 2, 2, 1, 2, 2) if harmonic: pattern = (2, 1, 2, 2, 1, 3, 1) else: pattern = None step_count = 0 if pattern: for step in pattern: yield step else: for i in range(tones): yield 1 scale = [ g for g in gen( tones=tones, semitones=semitones, major=major, minor=minor, harmonic=harmonic, melodic=melodic, hemitonic=hemitonic, ) ] if offset: scale = scale[offset:] + scale[:offset] # descending goes in meta? return {"intervals": scale, "hemitonic": hemitonic, "meta": {}}
[docs] 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)
[docs] def __repr__(self): return f"<System semitones={self.semitones!r}>"
[docs] def TET(n, *, names=None, reference_index=0, period=2.0): """Create an N-tone equal temperament system. Each step divides the period into *n* equal parts. The frequency ratio between adjacent tones is ``period^(1/n)``. For standard tunings the period is 2.0 (octave). For exotic systems like Bohlen-Pierce, set ``period=3.0`` (tritave). Args: n: Number of equal divisions of the octave (e.g. 19, 24, 31, 53). names: Optional list of *n* tone name strings. If omitted, tones are numbered ``"0"`` through ``"n-1"``. reference_index: Index of the tone that corresponds to A440 (default 0, meaning tone "0" = A4 = 440 Hz). Returns: A :class:`System` instance. Example:: >>> edo19 = TET(19) >>> from pytheory import Tone >>> t = Tone("0", octave=4, system=edo19) >>> t.frequency # 440.0 Hz (tone 0 = A4) 440.0 >>> edo31 = TET(31) >>> t = Tone("18", octave=4, system=edo31) >>> t.frequency # 18 steps above A in 31-TET """ if names is not None: if len(names) != n: raise ValueError(f"Expected {n} names, got {len(names)}") tone_names = [(name,) for name in names] else: tone_names = [(str(i),) for i in range(n)] # Degrees: numbered, with no modal names degrees = [(f"degree {i+1}", ()) for i in range(n)] # Scales: chromatic (all steps = 1) plus MOS scales for common EDOs scale_data = { "chromatic": (n, {}), } # Add well-known scales for specific EDOs if n == 19: # 19-TET: major and minor have different step sizes # Major: 3 3 2 3 3 3 2 (sums to 19) # Minor: 3 2 3 3 2 3 3 scale_data["heptatonic"] = [7, { "major": {"intervals": (3, 3, 2, 3, 3, 3, 2)}, "minor": {"intervals": (3, 2, 3, 3, 2, 3, 3)}, "harmonic minor": {"intervals": (3, 2, 3, 3, 2, 4, 2)}, }] scale_data["pentatonic"] = [5, { "major pentatonic": {"intervals": (3, 3, 5, 3, 5)}, "minor pentatonic": {"intervals": (5, 3, 3, 5, 3)}, }] elif n == 24: # 24-TET (quarter-tone): standard 12-TET scales with doubled steps scale_data["heptatonic"] = [7, { "major": {"intervals": (4, 4, 2, 4, 4, 4, 2)}, "minor": {"intervals": (4, 2, 4, 4, 2, 4, 4)}, }] elif n == 31: # 31-TET: excellent approximation of quarter-comma meantone # Major: 5 5 3 5 5 5 3 (sums to 31) # Minor: 5 3 5 5 3 5 5 scale_data["heptatonic"] = [7, { "major": {"intervals": (5, 5, 3, 5, 5, 5, 3)}, "minor": {"intervals": (5, 3, 5, 5, 3, 5, 5)}, "harmonic minor": {"intervals": (5, 3, 5, 5, 3, 7, 3)}, }] scale_data["pentatonic"] = [5, { "major pentatonic": {"intervals": (5, 5, 8, 5, 8)}, "minor pentatonic": {"intervals": (8, 5, 5, 8, 5)}, }] elif n == 53: # 53-TET: nearly perfect fifths and thirds # Major: 9 9 4 9 9 9 4 (sums to 53) scale_data["heptatonic"] = [7, { "major": {"intervals": (9, 9, 4, 9, 9, 9, 4)}, "minor": {"intervals": (9, 4, 9, 9, 4, 9, 9)}, }] # Find C equivalent for c_index (reference_index is A, C is 3 steps in 12-TET) # Proportionally: C is 3/12 of the way around from A c_idx = round(n * 3 / 12) if n != 12 else 3 return System( tone_names=tone_names, degrees=degrees, scales=scale_data, c_index=c_idx, period=period, )
# ── 19-TET named system ── # Traditional note names for 19-TET: all 12 western notes plus # 7 quarter-tone positions (enharmonic splits) _19TET_NAMES = [ "A", "A#", "Bb", "B", "B#", "C", "C#", "Db", "D", "D#", "Eb", "E", "E#", "F", "F#", "Gb", "G", "G#", "Ab", ] # ── 31-TET named system ── # Adriaan Fokker's naming: sharps and flats are distinct pitches _31TET_NAMES = [ "A", "A↑", "A#", "Bb", "B↓", "B", "B↑", "C", "C↑", "C#", "Db", "D↓", "D", "D↑", "D#", "Eb", "E↓", "E", "E↑", "E#", "F", "F↑", "F#", "Gb", "G↓", "G", "G↑", "G#", "Ab", "A↓", "A♮", # enharmonic return (distinct from "A" by a diesis) ] SYSTEMS = { "western": System(tone_names=TONES["western"], degrees=DEGREES["western"]), "indian": System(tone_names=TONES["indian"], degrees=DEGREES["indian"], scales=INDIAN_SCALES[12], c_index=3), "arabic": System(tone_names=TONES["arabic"], degrees=DEGREES["arabic"], scales=ARABIC_SCALES[12], c_index=3), "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], c_index=3), "19-tet": TET(19, names=_19TET_NAMES), "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, ratios=SHRUTI_RATIOS), "maqam": System(tone_names=TONES_ARABIC_24, degrees=DEGREES_ARABIC_24, scales=ARABIC_24_SCALES, c_index=5, ratios=MAQAM_RATIOS), "slendro": System(tone_names=TONES_SLENDRO, degrees=DEGREES_SLENDRO, scales=SLENDRO_SCALES, c_index=1), "pelog": System(tone_names=TONES_PELOG, degrees=DEGREES_PELOG, scales=PELOG_SCALES, c_index=2), "thai": System(tone_names=TONES_THAI, degrees=DEGREES_THAI, scales=THAI_SCALES, c_index=0), "makam": System(tone_names=TONES_TURKISH, degrees=DEGREES_TURKISH, scales=TURKISH_SCALES, c_index=13), "carnatic": System(tone_names=TONES_CARNATIC, degrees=DEGREES_CARNATIC, scales=CARNATIC_SCALES, c_index=18), # Sa ≈ C, 18 steps from A # Bohlen-Pierce: 13 equal divisions of the tritave (3:1). # Genuinely alien — no octaves, no fifths, built on 3:5:7 harmonics. # Used by composers like Heinz Bohlen, Kees van Prooijen, Georg Hajdu. "bohlen-pierce": TET(13, period=3.0, names=[ "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", ]), }