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 """ """ Tests for the scale module """
from ziffers import scale
import pytest import pytest
from ziffers import scale
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -5,7 +5,6 @@ import operator
import random import random
from .defaults import DEFAULT_OPTIONS from .defaults import DEFAULT_OPTIONS
@dataclass @dataclass
class Meta: class Meta:
"""Abstract class for all Ziffers items""" """Abstract class for all Ziffers items"""
@ -30,6 +29,7 @@ class Item(Meta):
text: str text: str
@dataclass @dataclass
class Whitespace(Item): class Whitespace(Item):
"""Class for whitespace""" """Class for whitespace"""
@ -76,8 +76,8 @@ class Pitch(Event):
"""Class for pitch in time""" """Class for pitch in time"""
pitch_class: int = field(default=None) pitch_class: int = field(default=None)
duration: float = field(default=None)
octave: int = field(default=None) octave: int = field(default=None)
note: int = field(default=None)
@dataclass @dataclass
@ -101,6 +101,14 @@ class Chord(Event):
pitch_classes: list[Pitch] = field(default=None) 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 @dataclass
class Function(Event): class Function(Event):
"""Class for functions""" """Class for functions"""
@ -129,7 +137,7 @@ class Sequence(Meta):
next_item = self.values[self.local_index] next_item = self.values[self.local_index]
self.local_index += 1 self.local_index += 1
return next_item return next_item
else:
self.local_index = 0 self.local_index = 0
raise StopIteration raise StopIteration
@ -165,7 +173,9 @@ class Ziffers(Sequence):
options: dict = field(default_factory=DEFAULT_OPTIONS) options: dict = field(default_factory=DEFAULT_OPTIONS)
loop_i: int = 0 loop_i: int = 0
iterator: iter = field(default=None, repr=False) 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): def __post_init__(self):
super().__post_init__() super().__post_init__()
@ -222,7 +232,9 @@ class Ziffers(Sequence):
def pairs(self) -> list[tuple]: def pairs(self) -> list[tuple]:
"""Return list of pitches and durations""" """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]: def octaves(self) -> list[int]:
"""Return list of octaves""" """Return list of octaves"""

View File

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

View File

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

View File

@ -3,7 +3,8 @@
# pylint: disable=locally-disabled, no-name-in-module # pylint: disable=locally-disabled, no-name-in-module
import re import re
from math import floor 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: def note_to_midi(name: str) -> int:
"""Parse note name to midi """Parse note name to midi
@ -36,6 +37,7 @@ def get_scale(name: str) -> list[int]:
scale = SCALES.get(name.lower().capitalize(), SCALES["Chromatic"]) scale = SCALES.get(name.lower().capitalize(), SCALES["Chromatic"])
return list(map(int, str(scale))) return list(map(int, str(scale)))
# pylint: disable=locally-disabled, too-many-arguments # pylint: disable=locally-disabled, too-many-arguments
def note_from_pc( def note_from_pc(
root: int | str, root: int | str,
@ -80,3 +82,18 @@ def note_from_pc(
note = root + sum(intervals[0:pitch_class]) note = root + sum(intervals[0:pitch_class])
return note + (octave * sum(intervals)) + modifier 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 for the rules
?root: sequence -> start ?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 classes
pitch_class: prefix* pitch pitch_class: prefix* pitch
@ -12,6 +12,9 @@
// Chords // Chords
chord: pitch_class pitch_class+ 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 // Valid as integer
?number: SIGNED_NUMBER | random_integer | cyclic_number ?number: SIGNED_NUMBER | random_integer | cyclic_number