from ._statics import (
TEMPERAMENTS, TONES, DEGREES, SCALES,
INDIAN_SCALES, ARABIC_SCALES, JAPANESE_SCALES,
BLUES_SCALES, GAMELAN_SCALES, SYSTEMS,
)
[docs]
class System:
[docs]
def __init__(self, *, tone_names, degrees, scales=None):
self.tone_names = tone_names
self.degrees = degrees
self._scales = scales
if scales is None:
self._scales = SCALES[self.semitones]
@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])
@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):
# TODO: figure out how to make this work with monotonic.
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 __repr__(self):
return f"<System semitones={self.semitones!r}>"
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]),
"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]),
}