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,7 +137,7 @@ class Sequence(Meta):
next_item = self.values[self.local_index]
self.local_index += 1
return next_item
else:
self.local_index = 0
raise StopIteration
@ -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__()
@ -222,7 +232,9 @@ class Ziffers(Sequence):
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 [
(val.pitch_class, val.dur) for val in self.values if isinstance(val, Pitch)
]
def octaves(self) -> list[int]:
"""Return list of octaves"""

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,7 +3,8 @@
# 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
@ -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,
@ -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