diff --git a/defaults.ini b/defaults.ini new file mode 100644 index 0000000..8659fdd --- /dev/null +++ b/defaults.ini @@ -0,0 +1,4 @@ +[DEFAULT] +Duration = 0.25 +Scale = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] # Chromatic + diff --git a/ziffers/classes.py b/ziffers/classes.py index 10acb97..594409f 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -1,77 +1,204 @@ -from dataclasses import dataclass, asdict +from dataclasses import dataclass, field +import operator +from typing import Any @dataclass class Meta: + ''' Abstract class for all Ziffers items''' + def update(self, new_values): + ''' Update attributes from dict ''' + for key, value in new_values.items(): + if hasattr(self, key): + setattr(self, key, value) + +@dataclass +class Item(Meta): + ''' Class for all Ziffers text based items ''' text: str @dataclass -class DurationChange(Meta): +class DurationChange(Item): + ''' Class for changing duration ''' dur: float @dataclass -class OctaveChange(Meta): +class OctaveChange(Item): + ''' Class for changing octave ''' oct: int @dataclass -class OctaveMod(Meta): +class OctaveMod(Item): + ''' Class for modifying octave ''' oct: int @dataclass -class Event(Meta): +class Event(Item): + ''' Abstract class for events with duration ''' dur: float = None @dataclass class Pitch(Event): + ''' Class for pitch in time ''' pc: int = None dur: float = None oct: int = None @dataclass class RandomPitch(Event): + ''' Class for random pitch ''' pc: int = None @dataclass -class RandomPercent(Meta): +class RandomPercent(Item): + ''' Class for random percent ''' percent: float = None @dataclass class Chord(Event): + ''' Class for chords ''' pcs: list[Pitch] = None @dataclass class Function(Event): + ''' Class for functions ''' run: str = None -@dataclass -class Ziffers: - values: list[Event] - dict = asdict - text: str = None - def __post_init__(self): - self.text = self.collect_text() - def collect_text(self) -> str: - return "".join([val.text for val in self.values]) - def pcs(self) -> list[int]: - return [val.pc for val in self.values if type(val) is Pitch] +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): - values: list[Event] + ''' Class for sequences of items''' + values: list + text: str = None + _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 + + wrapper: str = None + _wrapper: str = field(default=None, init=False, repr=False) + + @dataclass_property + def wrapper(self) -> str: + return self._wrapper + + @wrapper.setter + def wrapper(self, wrapper: str) -> None: + self._wrapper = wrapper + if self.text != None: + self.text = self.wrapper[0] + self.text + self.wrapper[1] + + def __post_init__(self): + self.text = self.collect_text() + if self.text != None and self.wrapper != None: + self.text = self.wrapper[0] + self.text + self.wrapper[1] + + def update_values(self, new_values): + ''' Update value attributes from dict ''' + for key, value in new_values.items(): + for obj in self.values: + if key!="text" and hasattr(obj, key): + setattr(obj, key, value) + + def collect_text(self) -> str: + return "".join([val.text for val in self.values]) + + def pcs(self) -> 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 Subdivision(Meta): +class ListSequence(Sequence): + ''' Class for Ziffers list sequences ''' + prefix: dict = None + values: list = None + def __init__(self): + super.__init__() + if self.prefix!=None: + self.update(self.prefix) + +@dataclass +class Subdivision(Item): + ''' Class for subdivisions ''' values: list[Event] @dataclass class Cyclic(Sequence): + ''' Class for cyclic sequences''' cycle: int = 0 + 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] @dataclass -class RandomInteger(Meta): +class RandomInteger(Item): + ''' Class for random integer ''' min: int max: int @dataclass -class Range(Meta): +class Range(Item): + ''' Class for range ''' start: int - end: int \ No newline at end of file + end: int + +ops = { + '+' : operator.add, + '-' : operator.sub, + '*' : operator.mul, + '/' : operator.truediv, + '%' : operator.mod +} + +@dataclass +class Operator(Item): + ''' Class for math operators ''' + value: ... = field(init=False, repr=False) + def __post_init__(self): + self.value = ops[self.text] + +@dataclass +class ListOperation(Sequence): + ''' Class for list operations ''' + def run(self): + pass + +@dataclass +class Operation(Item): + ''' Class for lisp-like operations: (+ 1 2 3) etc. ''' + values: list + operator: operator + +@dataclass +class Eval(Sequence): + ''' Class for evaluation notation ''' + result: ... = None + def __post_init__(self): + super().__post_init__() + self.result = eval(self.text) + +@dataclass +class Atom(Item): + ''' Class for evaluable atoms''' + value: ... + +@dataclass +class Integer(Item): + ''' Class for integers ''' + value: int \ No newline at end of file diff --git a/ziffers/common.py b/ziffers/common.py index 3996faf..c786b4c 100644 --- a/ziffers/common.py +++ b/ziffers/common.py @@ -1,2 +1,14 @@ -def flatten(arr) -> list: - return flatten(arr[0]) + (flatten(arr[1:]) if len(arr) > 1 else []) if type(arr) is list else [arr] \ No newline at end of file +def flatten(arr) -> list: + ''' Flattens array''' + return flatten(arr[0]) + (flatten(arr[1:]) if len(arr) > 1 else []) if type(arr) is list else [arr] + +def sum_dict(arr) -> dict: + ''' Sums array of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4} ''' + result = arr[0] + for hash in arr[1:]: + for key in hash.keys(): + if key in result: + result[key] = result[key] + hash[key] + else: + result[key] = hash[key] + return result \ No newline at end of file diff --git a/ziffers/mapper.py b/ziffers/mapper.py index e5500a2..276fecb 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -1,16 +1,13 @@ from lark import Transformer from .classes import * -from .common import flatten +from .common import flatten, sum_dict from .defaults import default_durs +import operator class ZiffersTransformer(Transformer): - def root(self, items): - return Ziffers(flatten(items)) - - def list(self,items): - values = flatten(items[0].values) - return Sequence(values=values,text="("+"".join([val.text for val in values])+")") + def sequence(self,items): + return Sequence(values=flatten(items)) def random_integer(self,s): val = s[0][1:-1].split(",") @@ -22,19 +19,12 @@ class ZiffersTransformer(Transformer): def cycle(self, items): values = items[0].values - no_spaces = [val for val in values if type(val)!=Meta] - return Cyclic(values=no_spaces,text="<"+"".join([val.text for val in values])+">") + return Cyclic(values=values, wrapper="<>") def pc(self, s): if(len(s)>1): # Collect&sum prefixes from any order: _qee^s4 etc. - result = s[0] - for hash in s[1:]: - for key in hash.keys(): - if key in result: - result[key] = result[key] + hash[key] - else: - result[key] = hash[key] + result = sum_dict(s) return Pitch(**result) else: val = s[0] @@ -89,7 +79,7 @@ class ZiffersTransformer(Transformer): key = s[0] val = default_durs[key] dots = len(s)-1 - if(dots>1): + if(dots>0): val = val * (2.0-(1.0/(2*dots))) return [key+"."*dots,val] @@ -105,7 +95,7 @@ class ZiffersTransformer(Transformer): return chardur def WS(self,s): - return Meta(text=s[0]) + return Item(text=s[0]) def subdivision(self,items): values = flatten(items[0]) @@ -113,3 +103,50 @@ class ZiffersTransformer(Transformer): def subitems(self,s): return s + + # Eval rules + + def eval(self,s): + val = s[0] + return Eval(values=val,wrapper="{}") + + def operation(self,s): + return s + + def atom(self,s): + val = s[0].value + return Atom(value=val,text=val) + + # List rules + + def list(self,items): + if len(items)>1: + prefixes = sum_dict(items[0:-1]) + seq = items[-1] + seq.wrapper = "()" + seq.text = prefixes["text"] + seq.text + seq.update_values(prefixes) + return seq + else: + seq = items[0] + seq.wrapper = "()" + return seq + + def SIGNED_NUMBER(self, s): + val = s.value + return Integer(text=val,value=int(val)) + + def lisp_operation(self,s): + op = s[0] + values = s[1:] + return Operation(operator=op,values=values,text="(+"+"".join([v.text for v in values])+")") + + def operator(self,s): + val = s[0].value + return Operator(text=val) + + def list_items(self,s): + return Sequence(values=s) + + def list_op(self,s): + return ListOperation(values=s) \ No newline at end of file diff --git a/ziffers/parser.py b/ziffers/parser.py index 4862b59..83f801a 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -6,7 +6,7 @@ from lark import Lark grammar_path = Path(__file__).parent grammar = grammar_path / "ziffers.lark" -ziffers_parser = Lark.open(grammar, rel_to=__file__, start='value', parser='lalr', transformer=ZiffersTransformer()) +ziffers_parser = Lark.open(grammar, rel_to=__file__, start='root', parser='lalr', transformer=ZiffersTransformer()) def parse_expression(expr): return ziffers_parser.parse(expr) \ No newline at end of file diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index 106f7fa..574308a 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -1,29 +1,59 @@ + // Root for the rules + ?root: sequence + sequence: (pc | dur_change | oct_mod | oct_change | WS | chord | cycle | random_integer | random_pitch | random_percent | range | list | lisp_operation | list_op | subdivision | eval)* - ?value: root - root: (pc | dur_change | oct_mod | oct_change | WS | chord | cycle | random_integer | random_pitch | random_percent | range | list | subdivision)* - list: "(" root ")" - random_integer: /\(-?[0-9]+,-?[0-9]+\)/ - range: /-?[0-9]\.\.-?[0-9]/ - cycle: "<" root ">" + // Pitch classes pc: prefix* pitch - pitch: /-?[0-9TE]/ - random_pitch: "?" - random_percent: "%" prefix: (octave | duration_chars | escaped_decimal | escaped_octave) - oct_change: escaped_octave WS - escaped_octave: /<-?[0-9]>/ - oct_mod: octave WS - octave: /[_^]+/ - chord: pc pc+ + pitch: /-?[0-9TE]/ escaped_decimal: "<" decimal ">" - dur_change: (duration_chars | decimal) WS + escaped_octave: /<-?[0-9]>/ + octave: /[_^]+/ + + // Chords + chord: pc pc+ + + // List + list: prefix* "(" sequence ")" + + // Right recursive list operation + list_op: list (operator (list | number))+ + operator: /([\+\-\*\/%]|<<|>>)/ + ?number: SIGNED_NUMBER + + // Lisp like list operation + lisp_operation: "(" operator WS sequence ")" + + // Durations duration_chars: dotted_dur+ dotted_dur: dchar dot* decimal: /-?[0-9]+\.[0-9]+/ dchar: /[mklpdcwyhnqaefsxtgujzo]/ dot: "." - subitems: (pc | WS | chord | cycle | subdivision)* - subdivision: "[" subitems "]" + // Subdivision + subdivision: "[" subitems "]" + subitems: (pc | WS | chord | cycle | subdivision)* + + // Control characters modifying future events + oct_mod: octave WS + oct_change: escaped_octave WS + dur_change: (duration_chars | decimal) WS + + // Generative rules + random_integer: /\(-?[0-9]+,-?[0-9]+\)/ + range: /-?[0-9]\.\.-?[0-9]/ + cycle: "<" sequence ">" + random_pitch: "?" + random_percent: "%" + + // Rules for evaluating clauses inside {} + // TODO: Support for parenthesis? + eval: "{" operation "}" + operation: atom (operator atom)+ + atom: (SIGNED_NUMBER | DECIMAL | random_integer) + + %import common.NUMBER %import common.SIGNED_NUMBER + %import common.DECIMAL %import common.WS \ No newline at end of file