Added roman numerals
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
""" Tests for the scale module """
|
||||
from ziffers import scale
|
||||
import pytest
|
||||
from ziffers import scale
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user