Added roman numerals
This commit is contained in:
@ -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(
|
||||||
|
|||||||
@ -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"""
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user