Files
ziffers-python/ziffers/scale.py
2023-02-07 21:04:48 +02:00

100 lines
3.1 KiB
Python

""" Methods for calculating notes from scales and list of all intervals in scales"""
#!/usr/bin/env python3
# pylint: disable=locally-disabled, no-name-in-module
import re
from math import floor
from .defaults import SCALES, MODIFIERS, NOTE_TO_INTERVAL, ROMANS
def note_to_midi(name: str) -> int:
"""Parse note name to midi
Args:
name (str): Note name in scientific notation: [a-gA-G][#bs][1-9]
Returns:
int: Midi note
"""
items = re.match(r"^([a-gA-G])([#bs])?([1-9])?$", name)
if items is None:
return 60
values = items.groups()
octave = int(values[2]) if values[2] else 4
modifier = MODIFIERS[values[1]] if values[1] else 0
interval = NOTE_TO_INTERVAL[values[0].capitalize()]
return 12 + octave * 12 + interval + modifier
def get_scale(name: str) -> list[int]:
"""Get a scale from the global scale list
Args:
name (str): Name of the scale as named in https://allthescales.org/
Returns:
list: List of intervals in the scale
"""
scale = SCALES.get(name.lower().capitalize(), SCALES["Chromatic"])
return list(map(int, str(scale)))
# pylint: disable=locally-disabled, too-many-arguments
def note_from_pc(
root: int | str,
pitch_class: int,
intervals: str | list[int | float],
cents: bool = False,
octave: int = 0,
modifier: int = 0,
) -> int:
"""Resolve a pitch class into a note from a scale
Args:
root (int | str): Root of the scale in MIDI or scientific pitch notation
pitch_class (int): Pitch class to be resolved
intervals (str | list[int | float]): Name or Intervals for the scale
cents (bool, optional): Flag for interpreting intervals as cents. Defaults to False.
octave (int, optional): Default octave. Defaults to 0.
modifier (int, optional): Modifier for the pitch class (#=1, b=-1). Defaults to 0.
Returns:
int: Resolved MIDI note
"""
# Initialization
root = note_to_midi(root) if isinstance(root, str) else root
intervals = get_scale(intervals) if isinstance(intervals, str) else intervals
intervals = list(map(lambda x: x / 100), intervals) if cents else intervals
scale_length = len(intervals)
# Resolve pitch classes to the scale and calculate octave
if pitch_class >= scale_length or pitch_class < 0:
octave += floor(pitch_class / scale_length)
pitch_class = (
scale_length - (abs(pitch_class) % scale_length)
if pitch_class < 0
else pitch_class % scale_length
)
if pitch_class == scale_length:
pitch_class = 0
# Computing the result
note = root + sum(intervals[0:pitch_class])
return note + (octave * sum(intervals)) + modifier
def parse_roman(numeral: str):
"""Parse roman numeral from string"""
values = [ROMANS[val] for val in numeral]
result = 0
i = 0
while i < len(values):
if i < len(values) - 1 and values[i + 1] > values[i]:
result += values[i + 1] - values[i]
i += 2
else:
result += values[i]
i += 1
return result