diff --git a/tests/test_parser.py b/tests/test_parser.py index 95723e3..6482441 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -47,3 +47,7 @@ def test_parsing_text(pattern: str): ) def test_pcs(pattern: str, expected: list): assert parse_expression(pattern).pcs() == expected + +# TODO: Add tests for octaves +# ("__6 _0 _1 _2 _3 _4 _5 _6 0 1 2 3 4 5 6 ^0 ^1 ^2 ^3 ^4 ^5 ^6 ^^0", [-2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2]), +# ("_ 1 _2 <3>3 ^^4", [-1,-2,3,-1]), diff --git a/ziffers/classes.py b/ziffers/classes.py index bf1b197..01f3233 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -1,7 +1,9 @@ from dataclasses import dataclass, field import operator from typing import Any - +import random +from .defaults import DEFAULT_OPTIONS +import itertools @dataclass class Meta: @@ -12,7 +14,13 @@ class Meta: for key, value in new_values.items(): if hasattr(self, key): setattr(self, key, value) - + + def update_new(self, new_values): + """Updates new attributes from dict""" + for key, value in new_values.items(): + if hasattr(self, key): + if getattr(self,key) == None: + setattr(self, key, value) @dataclass class Item(Meta): @@ -25,33 +33,39 @@ class Item(Meta): class Whitespace(Item): """Class for whitespace""" + item_type: str = None + @dataclass class DurationChange(Item): """Class for changing duration""" - dur: float - + value: float + key: str = "duration" + item_type: str = "change" @dataclass class OctaveChange(Item): """Class for changing octave""" - oct: int + value: int + key: str = "octave" + item_type: str = "change" @dataclass class OctaveMod(Item): """Class for modifying octave""" - oct: int - + value: int + key: str = "octave" + item_type: str = "add" @dataclass class Event(Item): """Abstract class for events with duration""" - dur: float = None + duration: float = None @dataclass @@ -59,8 +73,8 @@ class Pitch(Event): """Class for pitch in time""" pc: int = None - dur: float = None - oct: int = None + duration: float = None + octave: int = None @dataclass @@ -90,37 +104,32 @@ class Function(Event): run: str = None - -class dataclass_property(property): # pylint: disable=invalid-name - """Hack for dataclass setters""" - - def __set__(self, __obj: Any, __value: Any) -> None: - if isinstance(__value, self.__class__): - return None - return super().__set__(__obj, __value) - - @dataclass class Sequence(Meta): """Class for sequences of items""" values: list[Item] - text: str = field(init=False) - _text: str = field(default=None, init=False, repr=False) - - @dataclass_property - def text(self) -> str: - return self._text - - @text.setter - def text(self, text: str) -> None: - self._text = text - + text: str = None wrap_start: str = field(default=None, repr=False) wrap_end: str = field(default=None, repr=False) + local_index: int = 0 def __post_init__(self): self.text = self.collect_text() + # TODO: Filter out whitespace if not needed? + # self.values = list(filter(lambda elm: not isinstance(elm, Whitespace), self.values)) + + def __iter__(self): + return self + + def __next__(self): + if self.local_index list[int]: - return [val.pc for val in self.values if type(val) is Pitch] - - def durations(self) -> list[float]: - return [val.dur for val in self.values if type(val) is Pitch] - - def pairs(self) -> list[tuple]: - return [(val.pc, val.dur) for val in self.values if type(val) is Pitch] - - @dataclass class ListSequence(Sequence): """Class for Ziffers list sequences""" @@ -182,7 +181,13 @@ class Cyclic(Sequence): def __post_init__(self): super().__post_init__() # TODO: Do spaced need to be filtered out? - self.values = [val for val in self.values if type(val) != Item] + self.values = [val for val in self.values if isinstance(val,Whitespace)] + + def value(self): + return self.values[self.cycle] + + def next_cycle(self, cycle: int): + self.cycle = self.cycle+1 @dataclass @@ -192,13 +197,22 @@ class RandomInteger(Item): min: int max: int + def __post_init__(self): + if self.min>self.max: + new_max = self.min + self.min = self.max + self.max = new_max + + def value(self): + return random.randint(self.min,self.max) + @dataclass class Range(Item): """Class for range""" - start: int - end: int + start: int = None + end: int = None ops = { @@ -281,3 +295,58 @@ class RepeatedSequence(Sequence): repeats: Item = None wrap_start: str = field(default="[:", repr=False) wrap_end: str = field(default=":]", repr=False) + +@dataclass +class Ziffers(Meta): + """Main class for holding options and the current state""" + + sequence: Sequence + options: dict = field(default_factory=DEFAULT_OPTIONS) + loop_i: int = 0 + current: Item = None + it: iter = None + + def __post_init__(self): + self.it = iter(self.sequence) + + def __iter__(self): + return self + + def __next__(self): + try: + self.current = next(self.it) + + # Skip whitespace and collect duration & octave changes + while isinstance(self.current,(Whitespace,DurationChange,OctaveChange,OctaveMod)): + if self.current.item_type == "change": + self.options[self.current.key] = self.current.value + elif self.current.item_type == "add": + self.options[self.current.key] = self.current.value + self.current = next(self.it) + + except StopIteration: # Start from the beginning + self.current = next(self.it) + + self.current.update_new(self.options) + + self.loop_i += 1 + return self.current + + def take(self,num: int) -> list: + return list(itertools.islice(iter(self), num)) + + def set_defaults(self,options: dict): + self.options = DEFAULT_OPTIONS | options + + # TODO: Handle options and generated values + def pcs(self) -> list[int]: + return [val.pc for val in self.sequence.values if isinstance(val,Pitch)] + + def durations(self) -> list[float]: + return [val.dur for val in self.sequence.values if isinstance(val,Pitch)] + + def pairs(self) -> list[tuple]: + return [(val.pc,val.dur) for val in self.sequence.values if isinstance(val,Pitch)] + + def octaves(self) -> list[int]: + return [val.octave for val in self.sequence.values if isinstance(val,Pitch)] \ No newline at end of file diff --git a/ziffers/defaults.py b/ziffers/defaults.py index fb0e24d..9545711 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -35,3 +35,8 @@ DEFAULT_DURS = { "o": 8 / 1920, # ~0.00416 "z": 0.0, # 0 } + +DEFAULT_OPTIONS = { + "octave": 0, + "duration": 0.25 +} \ No newline at end of file diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 74e5d95..66447da 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -7,7 +7,8 @@ import operator class ZiffersTransformer(Transformer): def start(self, items): - return Sequence(values=items[0]) + seq = Sequence(values=items[0]) + return Ziffers(sequence=seq,options={}) def sequence(self, items): return flatten(items) @@ -41,26 +42,26 @@ class ZiffersTransformer(Transformer): def oct_change(self, s): octave = s[0] - return [OctaveChange(oct=octave["oct"], text=octave["text"]), s[1]] + return [OctaveChange(value=octave["octave"], text=octave["text"]), s[1]] def oct_mod(self, s): octave = s[0] - return [OctaveMod(oct=octave["oct"], text=octave["text"]), s[1]] + return [OctaveMod(value=octave["octave"], text=octave["text"]), s[1]] def escaped_octave(self, s): value = s[0][1:-1] - return {"oct": int(value), "text": s[0].value} + return {"octave": int(value), "text": s[0].value} def octave(self, s): value = sum([1 if char == "^" else -1 for char in s[0].value]) - return {"oct": value, "text": s[0].value} + return {"octave": value, "text": s[0].value} def chord(self, s): return Chord(pcs=s, text="".join([val.text for val in s])) def dur_change(self, s): durs = s[0] - return DurationChange(dur=durs[1], text=durs[0]) + return DurationChange(value=durs[1], text=durs[0]) def char_change(self, s): chars = "" @@ -94,7 +95,7 @@ class ZiffersTransformer(Transformer): def duration_chars(self, s): durations = [val[1] for val in s] characters = "".join([val[0] for val in s]) - return {"dur": sum(durations), "text": characters} + return {"duration": sum(durations), "text": characters} def dotted_dur(self, s): key = s[0] @@ -106,7 +107,7 @@ class ZiffersTransformer(Transformer): def decimal(self, s): val = s[0] - return {"dur": float(val), "text": val.value} + return {"duration": float(val), "text": val.value} def dot(self, s): return "." diff --git a/ziffers/parser.py b/ziffers/parser.py index e3dfe50..4176de2 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -14,7 +14,45 @@ ziffers_parser = Lark.open( transformer=ZiffersTransformer(), ) - def parse_expression(expr: str): """Parse an expression using the Ziffers parser""" return ziffers_parser.parse(expr) + +def zparse(expr: str, opts: dict=None): + parsed = parse_expression(expr) + if opts: + parsed.set_defaults(opts) + return parsed + +def z0(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z1(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z2(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z3(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z3(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z4(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z5(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z6(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z7(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z8(expr: str, opts: dict=None): + return zparse(expr,opts) + +def z9(expr: str, opts: dict=None): + return zparse(expr,opts) \ No newline at end of file