Added roman numerals

This commit is contained in:
2023-02-07 21:04:48 +02:00
parent 1c4dfb99a0
commit 04d84bcc47
6 changed files with 93 additions and 27 deletions

View File

@ -1,6 +1,7 @@
""" Tests for the scale module """
from ziffers import scale
import pytest
from ziffers import scale
@pytest.mark.parametrize(

View File

@ -5,7 +5,6 @@ import operator
import random
from .defaults import DEFAULT_OPTIONS
@dataclass
class Meta:
"""Abstract class for all Ziffers items"""
@ -30,6 +29,7 @@ class Item(Meta):
text: str
@dataclass
class Whitespace(Item):
"""Class for whitespace"""
@ -76,8 +76,8 @@ class Pitch(Event):
"""Class for pitch in time"""
pitch_class: int = field(default=None)
duration: float = field(default=None)
octave: int = field(default=None)
note: int = field(default=None)
@dataclass
@ -101,6 +101,14 @@ class Chord(Event):
pitch_classes: list[Pitch] = field(default=None)
@dataclass
class RomanNumeral(Event):
"""Class for roman numbers"""
value: str = field(default=None)
chord_type: str = field(default=None)
@dataclass
class Function(Event):
"""Class for functions"""
@ -129,9 +137,9 @@ class Sequence(Meta):
next_item = self.values[self.local_index]
self.local_index += 1
return next_item
else:
self.local_index = 0
raise StopIteration
self.local_index = 0
raise StopIteration
def update_values(self, new_values):
"""Update value attributes from dict"""
@ -165,7 +173,9 @@ class Ziffers(Sequence):
options: dict = field(default_factory=DEFAULT_OPTIONS)
loop_i: int = 0
iterator: iter = field(default=None, repr=False)
current: Whitespace|DurationChange|OctaveChange|OctaveAdd = field(default=None)
current: Whitespace | DurationChange | OctaveChange | OctaveAdd = field(
default=None
)
def __post_init__(self):
super().__post_init__()
@ -193,7 +203,7 @@ class Ziffers(Sequence):
return self.current
def take(self, num: int) -> list[Pitch]:
""" Take number of pitch classes from the parsed sequence. Cycles from the beginning.
"""Take number of pitch classes from the parsed sequence. Cycles from the beginning.
Args:
num (int): Number of pitch classes to take from the sequence
@ -204,7 +214,7 @@ class Ziffers(Sequence):
return list(itertools.islice(itertools.cycle(self), num))
def set_defaults(self, options: dict):
""" Sets options for the parser
"""Sets options for the parser
Args:
options (dict): Options as a dict
@ -213,19 +223,21 @@ class Ziffers(Sequence):
# TODO: Refactor these
def pitch_classes(self) -> list[int]:
""" Return list of pitch classes as ints """
"""Return list of pitch classes as ints"""
return [val.pitch_class for val in self.values if isinstance(val, Pitch)]
def durations(self) -> list[float]:
""" Return list of pitch durations as floats"""
"""Return list of pitch durations as floats"""
return [val.dur for val in self.values if isinstance(val, Pitch)]
def pairs(self) -> list[tuple]:
""" Return list of pitches and durations """
return [(val.pitch_class, val.dur) for val in self.values if isinstance(val, Pitch)]
"""Return list of pitches and durations"""
return [
(val.pitch_class, val.dur) for val in self.values if isinstance(val, Pitch)
]
def octaves(self) -> list[int]:
""" Return list of octaves """
"""Return list of octaves"""
return [val.octave for val in self.values if isinstance(val, Pitch)]
@ -258,7 +270,7 @@ class RandomInteger(Item):
self.max = new_max
def value(self):
""" Evaluate the random value for the generator """
"""Evaluate the random value for the generator"""
return random.randint(self.min, self.max)
@ -292,11 +304,11 @@ class Cyclic(Sequence):
self.values = [val for val in self.values if isinstance(val, Whitespace)]
def value(self):
""" Get the value for the current cycle """
"""Get the value for the current cycle"""
return self.values[self.cycle]
def next_cycle(self, cycle: int):
""" Evaluate next cycle """
"""Evaluate next cycle"""
self.cycle = self.cycle + 1
@ -332,7 +344,7 @@ class ListOperation(Sequence):
"""Class for list operations"""
def run(self):
""" Run operations """
"""Run operations"""
pass

View File

@ -51,6 +51,16 @@ MODIFIERS = {
"s": 1,
}
ROMANS = {
'i': 1,
'v': 5,
'x': 10,
'l': 50,
'c': 100,
'd': 500,
'm': 1000
}
# pylint: disable=locally-disabled, too-many-lines
SCALES = {

View File

@ -1,4 +1,5 @@
""" Lark transformer for mapping Lark tokens to Ziffers objects """
from typing import Optional
from lark import Transformer
from .classes import (
Ziffers,
@ -10,6 +11,7 @@ from .classes import (
RandomPitch,
RandomPercent,
Chord,
RomanNumeral,
Sequence,
ListSequence,
RepeatedListSequence,
@ -28,13 +30,18 @@ from .classes import (
)
from .common import flatten, sum_dict
from .defaults import DEFAULT_DURS
from .scale import note_from_pc, parse_roman
# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name
class ZiffersTransformer(Transformer):
"""Rules for transforming Ziffers expressions into tree."""
def start(self, items):
def __init__(self, options: Optional[dict]=None):
super().__init__()
self.options = options
def start(self, items) -> Ziffers:
"""Root for the rules"""
seq = Sequence(values=items[0])
return Ziffers(values=seq, options={})
@ -43,17 +50,17 @@ class ZiffersTransformer(Transformer):
"""Flatten sequence"""
return flatten(items)
def random_integer(self, item):
def random_integer(self, item) -> RandomInteger:
"""Parses random integer syntax"""
val = item[0][1:-1].split(",")
return RandomInteger(min=val[0], max=val[1], text=item[0].value)
def range(self, item):
def range(self, item) -> Range:
"""Parses range syntax"""
val = item[0].split("..")
return Range(start=val[0], end=val[1], text=item[0])
def cycle(self, items):
def cycle(self, items) -> Cyclic:
"""Parses cycle"""
values = items[0]
return Cyclic(values=values)
@ -102,6 +109,22 @@ class ZiffersTransformer(Transformer):
"""Parses chord"""
return Chord(pitch_classes=items, text="".join([val.text for val in items]))
def named_roman(self,items) -> RomanNumeral:
"""Parse chord from roman numeral"""
numeral = items[0].value
if len(items)>1:
name = items[1]
return RomanNumeral(text=numeral, value=parse_roman(numeral), chord_type=name)
return RomanNumeral(value=parse_roman(numeral), text=numeral)
def chord_name(self,item):
"""Return name for chord"""
return item[0].value
def roman_number(self,item):
"""Return roman numeral"""
return item.value
def dur_change(self, items):
"""Parses duration change"""
durs = items[0]

View File

@ -3,10 +3,11 @@
# pylint: disable=locally-disabled, no-name-in-module
import re
from math import floor
from .defaults import SCALES, MODIFIERS, NOTE_TO_INTERVAL
from .defaults import SCALES, MODIFIERS, NOTE_TO_INTERVAL, ROMANS
def note_to_midi(name: str) -> int:
""" Parse note name to midi
"""Parse note name to midi
Args:
name (str): Note name in scientific notation: [a-gA-G][#bs][1-9]
@ -25,7 +26,7 @@ def note_to_midi(name: str) -> int:
def get_scale(name: str) -> list[int]:
""" Get a scale from the global scale list
"""Get a scale from the global scale list
Args:
name (str): Name of the scale as named in https://allthescales.org/
@ -36,6 +37,7 @@ def get_scale(name: str) -> list[int]:
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,
@ -45,7 +47,7 @@ def note_from_pc(
octave: int = 0,
modifier: int = 0,
) -> int:
""" Resolve a pitch class into a note from a scale
"""Resolve a pitch class into a note from a scale
Args:
root (int | str): Root of the scale in MIDI or scientific pitch notation
@ -80,3 +82,18 @@ def note_from_pc(
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

View File

@ -1,6 +1,6 @@
// Root for the rules
?root: sequence -> start
sequence: (pitch_class | dur_change | oct_mod | oct_change | WS | chord | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)*
sequence: (pitch_class | dur_change | oct_mod | oct_change | WS | chord | named_roman | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)*
// Pitch classes
pitch_class: prefix* pitch
@ -12,6 +12,9 @@
// Chords
chord: pitch_class pitch_class+
named_roman: roman_number (("^" chord_name) | ("+" number))?
chord_name: /[a-zA-Z0-9]+/
?roman_number: /iv|v|v?i{1,3}/
// Valid as integer
?number: SIGNED_NUMBER | random_integer | cyclic_number