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,9 +137,9 @@ 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
def update_values(self, new_values): def update_values(self, new_values):
"""Update value attributes from dict""" """Update value attributes from dict"""
@ -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__()
@ -193,7 +203,7 @@ class Ziffers(Sequence):
return self.current return self.current
def take(self, num: int) -> list[Pitch]: 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: Args:
num (int): Number of pitch classes to take from the sequence 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)) return list(itertools.islice(itertools.cycle(self), num))
def set_defaults(self, options: dict): def set_defaults(self, options: dict):
""" Sets options for the parser """Sets options for the parser
Args: Args:
options (dict): Options as a dict options (dict): Options as a dict
@ -213,19 +223,21 @@ class Ziffers(Sequence):
# TODO: Refactor these # TODO: Refactor these
def pitch_classes(self) -> list[int]: 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)] return [val.pitch_class for val in self.values if isinstance(val, Pitch)]
def durations(self) -> list[float]: 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)] return [val.dur for val in self.values if isinstance(val, Pitch)]
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"""
return [val.octave for val in self.values if isinstance(val, Pitch)] return [val.octave for val in self.values if isinstance(val, Pitch)]
@ -258,7 +270,7 @@ class RandomInteger(Item):
self.max = new_max self.max = new_max
def value(self): def value(self):
""" Evaluate the random value for the generator """ """Evaluate the random value for the generator"""
return random.randint(self.min, self.max) 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)] self.values = [val for val in self.values if isinstance(val, Whitespace)]
def value(self): def value(self):
""" Get the value for the current cycle """ """Get the value for the current cycle"""
return self.values[self.cycle] return self.values[self.cycle]
def next_cycle(self, cycle: int): def next_cycle(self, cycle: int):
""" Evaluate next cycle """ """Evaluate next cycle"""
self.cycle = self.cycle + 1 self.cycle = self.cycle + 1
@ -332,7 +344,7 @@ class ListOperation(Sequence):
"""Class for list operations""" """Class for list operations"""
def run(self): def run(self):
""" Run operations """ """Run operations"""
pass pass

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,10 +3,11 @@
# 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
Args: Args:
name (str): Note name in scientific notation: [a-gA-G][#bs][1-9] 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]: def get_scale(name: str) -> list[int]:
""" Get a scale from the global scale list """Get a scale from the global scale list
Args: Args:
name (str): Name of the scale as named in https://allthescales.org/ 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"]) 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,
@ -45,7 +47,7 @@ def note_from_pc(
octave: int = 0, octave: int = 0,
modifier: int = 0, modifier: int = 0,
) -> int: ) -> int:
""" Resolve a pitch class into a note from a scale """Resolve a pitch class into a note from a scale
Args: Args:
root (int | str): Root of the scale in MIDI or scientific pitch notation 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]) 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