diff --git a/main.py b/main.py index eae0fc9..1274101 100644 --- a/main.py +++ b/main.py @@ -3,38 +3,38 @@ from rich import print if __name__ == "__main__": expressions = { - 'Pitches': "-2 -1 0 1 2", - 'Chords': "0 024 2 246", - 'Note lengths': "w 0 h 1 q 2 e 3 s 4", - 'Subdivision': "[1 2 [3 4]]", - 'Decimal durations': "0.25 0 1 <0.333>2 3", - 'Octaves': "^ 0 ^ 1 _ 2 _ 3", - 'Escaped octave': "<2> 1 <1>1<-2>3", - 'Roman chords': "i ii iii+4 iv+5 v+8 vi+10 vii+20", - 'Named chords': "i^7 i^min i^dim i^maj7", - 'Modal interchange (a-g)': "iiia ig ivf^7", - 'Escape/eval': "{10 11} {1.2 2.43} {3+1*2}", - 'Randoms': "% ? % ? % ?", - 'Random between': "(-3,6)", - 'Random selections': "[q 1 2, q 3 e 4 6]", - 'Repeat': "[: 1 (2,6) 3 :4]", - 'Repeat cycles': "[: (1,4) <(2 3) (3 (1,7))> :]", - 'Lists': "h 1 q(0 1 2 3) 2", - 'List cycles': "(: (1,4) <(2 3) (3 (1,7))> :)", - 'Loop cycles (for zloop or z0-z9)': "<0 <1 <2 <3 <4 5>>>>>", - 'Basic operations': "(1 2 (3 4)+2)*2 ((1 2 3)+(0 9 13))-2 ((3 4 {10})*(2 9 3))%7", - 'Product operations': "(0 1 2 3)+(1 4 2 3) (0 1 2)-(0 2 1)+2", - 'Euclid cycles': "(q1)<6,7>(q4 (e3 e4) q2) (q1)<6,7>(q4 q3 q2)", - 'Transformations': "(0 1 2) (0 1 2)(-2 1)", - 'List assignation': "A=(0 (1,6) 3) B=(3 ? 2) B A B B A", - 'Random repeat': "(: 1 (2,6) 3 :4)", - 'Conditionals': "1 {%<0.5?3} 3 4 (: 1 2 {%<0.2?3:2} :3)", - 'Functions': "(0 1 2 3){x%3==0?x-2:x+2}", - 'Polynomials': "(-10..10){(x**3)*(x+1)%12}", + "Pitches": "-2 -1 0 1 2", + "Chords": "0 024 2 246", + "Note lengths": "w 0 h 1 q 2 e 3 s 4", + "Subdivision": "[1 2 [3 4]]", + "Decimal durations": "0.25 0 1 <0.333>2 3", + "Octaves": "^ 0 ^ 1 _ 2 _ 3", + "Escaped octave": "<2> 1 <1>1<-2>3", + "Roman chords": "i ii iii+4 iv+5 v+8 vi+10 vii+20", + "Named chords": "i^7 i^min i^dim i^maj7", + "Modal interchange (a-g)": "iiia ig ivf^7", + "Escape/eval": "{10 11} {1.2 2.43} {3+1*2}", + "Randoms": "% ? % ? % ?", + "Random between": "(-3,6)", + "Random selections": "[q 1 2, q 3 e 4 6]", + "Repeat": "[: 1 (2,6) 3 :4]", + "Repeat cycles": "[: (1,4) <(2 3) (3 (1,7))> :]", + "Lists": "h 1 q(0 1 2 3) 2", + "List cycles": "(: (1,4) <(2 3) (3 (1,7))> :)", + "Loop cycles (for zloop or z0-z9)": "<0 <1 <2 <3 <4 5>>>>>", + "Basic operations": "(1 2 (3 4)+2)*2 ((1 2 3)+(0 9 13))-2 ((3 4 {10})*(2 9 3))%7", + "Product operations": "(0 1 2 3)+(1 4 2 3) (0 1 2)-(0 2 1)+2", + "Euclid cycles": "(q1)<6,7>(q4 (e3 e4) q2) (q1)<6,7>(q4 q3 q2)", + "Transformations": "(0 1 2) (0 1 2)(-2 1)", + "List assignation": "A=(0 (1,6) 3) B=(3 ? 2) B A B B A", + "Random repeat": "(: 1 (2,6) 3 :4)", + "Conditionals": "1 {%<0.5?3} 3 4 (: 1 2 {%<0.2?3:2} :3)", + "Functions": "(0 1 2 3){x%3==0?x-2:x+2}", + "Polynomials": "(-10..10){(x**3)*(x+1)%12}", } for ex in expressions: try: - print(f"Parsed: "+parse_expression(expressions[ex]).text) + print(f"Parsed: " + parse_expression(expressions[ex]).text) except Exception as e: print(f"[red]Failed on {ex}[/red]") - #print(f"[red]Failed on {ex}[/red]: {str(e)[0:40]}...") + # print(f"[red]Failed on {ex}[/red]: {str(e)[0:40]}...") diff --git a/repl.py b/repl.py index 1bb5f29..7df60f4 100644 --- a/repl.py +++ b/repl.py @@ -1,12 +1,12 @@ from ziffers import * from rich import print -EXIT_CONDITION = ('exit', 'quit', '') +EXIT_CONDITION = ("exit", "quit", "") if __name__ == "__main__": print(f"[purple]== ZIFFERS REPL ==[/purple]") while True: - expr = input('> ') + expr = input("> ") if expr not in EXIT_CONDITION: try: result = parse_expression(expr) diff --git a/tests/test_parser.py b/tests/test_parser.py index f06e7dd..95723e3 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,15 +1,16 @@ from ziffers import * import pytest + def test_can_parse(): expressions = [ - "[1 [2 3]]", - "(1 (1,3) 1..3)", - "_^ q _qe^3 qww_4 _123 <1 2>", - "q _2 _ 3 ^ 343", - "2 qe2 e4", - "q 2 <3 343>", - "q (2 <3 343 (3 4)>)", + "[1 [2 3]]", + "(1 (1,3) 1..3)", + "_^ q _qe^3 qww_4 _123 <1 2>", + "q _2 _ 3 ^ 343", + "2 qe2 e4", + "q 2 <3 343>", + "q (2 <3 343 (3 4)>)", ] results = [] for expression in expressions: @@ -21,10 +22,11 @@ def test_can_parse(): print(e) results.append(False) - # Return true if all the results are true (succesfully parsed) + # Return true if all the results are true (succesfully parsed) print(results) assert all(results) + @pytest.mark.parametrize( "pattern", [ @@ -35,12 +37,13 @@ def test_can_parse(): def test_parsing_text(pattern: str): assert parse_expression(pattern).text == pattern + @pytest.mark.parametrize( "pattern,expected", [ - ("1 2 3", [1,2,3]), - ("q2 eq3", [2,3]), + ("1 2 3", [1, 2, 3]), + ("q2 eq3", [2, 3]), ], ) def test_pcs(pattern: str, expected: list): - assert parse_expression(pattern).pcs() == expected \ No newline at end of file + assert parse_expression(pattern).pcs() == expected diff --git a/tests/test_scale.py b/tests/test_scale.py index 4571f9b..9256e9a 100644 --- a/tests/test_scale.py +++ b/tests/test_scale.py @@ -1,6 +1,7 @@ from ziffers import scale import pytest + @pytest.mark.parametrize( "name,expected", [ @@ -14,11 +15,37 @@ import pytest def test_notenames(name: str, expected: int): assert scale.note_to_midi(name) == expected + @pytest.mark.parametrize( "pcs,expected", [ - (list(range(-9,10)), [45, 47, 48, 50, 52, 53, 55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72, 74, 76]), + ( + list(range(-9, 10)), + [ + 45, + 47, + 48, + 50, + 52, + 53, + 55, + 57, + 59, + 60, + 62, + 64, + 65, + 67, + 69, + 71, + 72, + 74, + 76, + ], + ), ], ) def test_note_to_midi(pcs: str, expected: int): - assert [scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian") for val in pcs] == expected + assert [ + scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian") for val in pcs + ] == expected diff --git a/ziffers/classes.py b/ziffers/classes.py index cff89a9..bf1b197 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -2,81 +2,108 @@ from dataclasses import dataclass, field import operator from typing import Any + @dataclass class Meta: - ''' Abstract class for all Ziffers items''' + """Abstract class for all Ziffers items""" + def update(self, new_values): - ''' Update attributes from dict ''' + """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 ''' + """Class for all Ziffers text based items""" + text: str + @dataclass class Whitespace(Item): - ''' Class for whitespace ''' + """Class for whitespace""" + @dataclass class DurationChange(Item): - ''' Class for changing duration ''' + """Class for changing duration""" + dur: float + @dataclass class OctaveChange(Item): - ''' Class for changing octave ''' + """Class for changing octave""" + oct: int + @dataclass class OctaveMod(Item): - ''' Class for modifying octave ''' + """Class for modifying octave""" + oct: int + @dataclass class Event(Item): - ''' Abstract class for events with duration ''' + """Abstract class for events with duration""" + dur: float = None + @dataclass class Pitch(Event): - ''' Class for pitch in time ''' + """Class for pitch in time""" + pc: int = None dur: float = None oct: int = None + @dataclass class RandomPitch(Event): - ''' Class for random pitch ''' + """Class for random pitch""" + pc: int = None + @dataclass class RandomPercent(Item): - ''' Class for random percent ''' + """Class for random percent""" + percent: float = None + @dataclass class Chord(Event): - ''' Class for chords ''' + """Class for chords""" + pcs: list[Pitch] = None - + + @dataclass class Function(Event): - ''' Class for functions ''' + """Class for functions""" + run: str = None -class dataclass_property(property): # pylint: disable=invalid-name - ''' Hack for dataclass setters ''' + +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''' + """Class for sequences of items""" + values: list[Item] text: str = field(init=False) _text: str = field(default=None, init=False, repr=False) @@ -96,12 +123,12 @@ class Sequence(Meta): self.text = self.collect_text() def update_values(self, new_values): - ''' Update value attributes from dict ''' + """Update value attributes from dict""" for key, value in new_values.items(): for obj in self.values: - if key!="text" and hasattr(obj, key): + if key != "text" and hasattr(obj, key): setattr(obj, key, value) - + def collect_text(self) -> str: text = "".join([val.text for val in self.values]) if self.wrap_start != None: @@ -112,115 +139,145 @@ class Sequence(Meta): 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] + return [(val.pc, val.dur) for val in self.values if type(val) is Pitch] + @dataclass class ListSequence(Sequence): - ''' Class for Ziffers list sequences ''' + """Class for Ziffers list sequences""" + wrap_start: str = field(default="(", repr=False) wrap_end: str = field(default=")", repr=False) + @dataclass class RepeatedListSequence(Sequence): - ''' Class for Ziffers list sequences ''' + """Class for Ziffers list sequences""" + repeats: Item = None wrap_start: str = field(default="(:", repr=False) wrap_end: str = field(default=":)", repr=False) + @dataclass class Subdivision(Item): - ''' Class for subdivisions ''' + """Class for subdivisions""" + values: list[Event] + @dataclass class Cyclic(Sequence): - ''' Class for cyclic sequences''' + """Class for cyclic sequences""" + cycle: int = 0 wrap_start: str = field(default="<", repr=False) wrap_end: str = field(default=">", repr=False) - + 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 type(val) != Item] + @dataclass class RandomInteger(Item): - ''' Class for random integer ''' + """Class for random integer""" + min: int max: int + @dataclass class Range(Item): - ''' Class for range ''' + """Class for range""" + start: int end: int + ops = { - '+' : operator.add, - '-' : operator.sub, - '*' : operator.mul, - '/' : operator.truediv, - '%' : operator.mod + "+": operator.add, + "-": operator.sub, + "*": operator.mul, + "/": operator.truediv, + "%": operator.mod, } + @dataclass class Operator(Item): - ''' Class for math operators ''' - value: ... = field(init=False, repr=False) + """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 ''' + """Class for list operations""" + def run(self): pass + @dataclass class Operation(Item): - ''' Class for lisp-like operations: (+ 1 2 3) etc. ''' + """Class for lisp-like operations: (+ 1 2 3) etc.""" + values: list operator: operator + @dataclass class Eval(Sequence): - ''' Class for evaluation notation ''' + """Class for evaluation notation""" + result: ... = None wrap_start: str = field(default="{", repr=False) wrap_end: str = field(default="}", repr=False) + def __post_init__(self): super().__post_init__() self.result = eval(self.text) + @dataclass class Atom(Item): - ''' Class for evaluable atoms''' + """Class for evaluable atoms""" + value: ... + @dataclass class Integer(Item): - ''' Class for integers ''' + """Class for integers""" + value: int + @dataclass class Euclid(Item): - ''' Class for euclidean cycles ''' + """Class for euclidean cycles""" + pulses: int length: int onset: list offset: list = None rotate: int = None + @dataclass class RepeatedSequence(Sequence): - ''' Class for repeats ''' + """Class for repeats""" + repeats: Item = None wrap_start: str = field(default="[:", repr=False) wrap_end: str = field(default=":]", repr=False) - \ No newline at end of file diff --git a/ziffers/common.py b/ziffers/common.py index c786b4c..e2771a5 100644 --- a/ziffers/common.py +++ b/ziffers/common.py @@ -1,14 +1,19 @@ 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] + """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} ''' + """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(): + 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 + return result diff --git a/ziffers/defaults.py b/ziffers/defaults.py index 56f598b..5450226 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -1,37 +1,37 @@ default_durs = { - 'm': 8.0, # 15360/1920 - 'k': 10240/1920, # ~5.333 - 'l': 4.0, # 7680/1920 - 'd.': 3.0, # - 'p': 5120/1920, # ~2.666 - 'd': 2.0, # 3840/1920 - 'w.': 1.5, # 2280/1920 - 'c': 2560/1920, # ~1.333 - 'w': 1.0, # 1920/1920 - 'h..': 0.875, # 1680/1920 - 'h.': 0.75, # 1440/1920 - 'y': 1280/1920, # ~0.666 - 'h': 0.5, # 960/1920 - 1/2 - 'q..': 840/1920, # ~0.4375 - 'q.': 0.375, # 720/1920 - 'n': 640/1920, # ~0.333 - 'q': 0.25, # 480/1920 - 1/4 - 'e..': 420/1920, # = 0.218 - 'e.': 0.1875, # 360/1920 - 'a': 320/1920, # 0.167 - 1/8 - 'e': 0.125, # 240/1920 - 's..': 210/1920, # ~0.10937 - 's.': 180/1920, # ~0.0937 - 'f': 160/1920, # ~0.083 - 1/16 - 's': 0.0625, # 120/1920 - 't..': 105/1920, # ~0.0546 - 't.': 90/1920, # ~0.0468 - 'x': 80/1920, # ~0.042 - 1/32 - 't': 60/1920, # ~0.031 - 'u.': 45/1920, # ~0.023 - 'g': 40/1920, # ~0.021 - 1/64 - 'u': 30/1920, # ~0.016 - 'j': 15/1920, # ~0.0078 - 1/128 - 'o': 8/1920, # ~0.00416 - 'z': 0.0 # 0 - } + "m": 8.0, # 15360/1920 + "k": 10240 / 1920, # ~5.333 + "l": 4.0, # 7680/1920 + "d.": 3.0, # + "p": 5120 / 1920, # ~2.666 + "d": 2.0, # 3840/1920 + "w.": 1.5, # 2280/1920 + "c": 2560 / 1920, # ~1.333 + "w": 1.0, # 1920/1920 + "h..": 0.875, # 1680/1920 + "h.": 0.75, # 1440/1920 + "y": 1280 / 1920, # ~0.666 + "h": 0.5, # 960/1920 - 1/2 + "q..": 840 / 1920, # ~0.4375 + "q.": 0.375, # 720/1920 + "n": 640 / 1920, # ~0.333 + "q": 0.25, # 480/1920 - 1/4 + "e..": 420 / 1920, # = 0.218 + "e.": 0.1875, # 360/1920 + "a": 320 / 1920, # 0.167 - 1/8 + "e": 0.125, # 240/1920 + "s..": 210 / 1920, # ~0.10937 + "s.": 180 / 1920, # ~0.0937 + "f": 160 / 1920, # ~0.083 - 1/16 + "s": 0.0625, # 120/1920 + "t..": 105 / 1920, # ~0.0546 + "t.": 90 / 1920, # ~0.0468 + "x": 80 / 1920, # ~0.042 - 1/32 + "t": 60 / 1920, # ~0.031 + "u.": 45 / 1920, # ~0.023 + "g": 40 / 1920, # ~0.021 - 1/64 + "u": 30 / 1920, # ~0.016 + "j": 15 / 1920, # ~0.0078 - 1/128 + "o": 8 / 1920, # ~0.00416 + "z": 0.0, # 0 +} diff --git a/ziffers/mapper.py b/ziffers/mapper.py index e4bfcfb..0f7ddb8 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -4,211 +4,231 @@ from .common import flatten, sum_dict from .defaults import default_durs import operator + class ZiffersTransformer(Transformer): - - def start(self,items): + def start(self, items): return Sequence(values=items[0]) - def sequence(self,items): + def sequence(self, items): return flatten(items) - def random_integer(self,s): + def random_integer(self, s): val = s[0][1:-1].split(",") - return RandomInteger(min=val[0],max=val[1],text=s[0].value) + return RandomInteger(min=val[0], max=val[1], text=s[0].value) - def range(self,s): + def range(self, s): val = s[0].split("..") - return Range(start=val[0],end=val[1],text=s[0]) - + return Range(start=val[0], end=val[1], text=s[0]) + def cycle(self, items): values = items[0] return Cyclic(values=values) def pc(self, s): - if(len(s)>1): + if len(s) > 1: # Collect&sum prefixes from any order: _qee^s4 etc. result = sum_dict(s) return Pitch(**result) else: val = s[0] - return Pitch(**val) + return Pitch(**val) - def pitch(self,s): - return {"pc":int(s[0].value),"text":s[0].value} + def pitch(self, s): + return {"pc": int(s[0].value), "text": s[0].value} - def prefix(self,s): + def prefix(self, s): return s[0] - def oct_change(self,s): + def oct_change(self, s): octave = s[0] - return [OctaveChange(oct=octave["oct"],text=octave["text"]),s[1]] + return [OctaveChange(oct=octave["oct"], text=octave["text"]), s[1]] - def oct_mod(self,s): + def oct_mod(self, s): octave = s[0] - return [OctaveMod(oct=octave["oct"],text=octave["text"]),s[1]] + return [OctaveMod(oct=octave["oct"], text=octave["text"]), s[1]] - def escaped_octave(self,s): + def escaped_octave(self, s): value = s[0][1:-1] - return {"oct": int(value), "text":s[0].value} + return {"oct": 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} + def octave(self, s): + value = sum([1 if char == "^" else -1 for char in s[0].value]) + return {"oct": value, "text": s[0].value} - def chord(self,s): - return Chord(pcs=s,text="".join([val.text for val in s])) + def chord(self, s): + return Chord(pcs=s, text="".join([val.text for val in s])) - def dur_change(self,s): + def dur_change(self, s): durs = s[0] return DurationChange(dur=durs[1], text=durs[0]) - def char_change(self,s): + def char_change(self, s): chars = "" durs = 0.0 - for (dchar,dots) in s: + for (dchar, dots) in s: val = default_durs[dchar] - if(dots>0): - val = val * (2.0-(1.0/(2*dots))) - chars = chars + (dchar+"."*dots) + if dots > 0: + val = val * (2.0 - (1.0 / (2 * dots))) + chars = chars + (dchar + "." * dots) durs = durs + val - return [chars,durs] + return [chars, durs] - def dchar_not_prefix(self,s): - dur = s[0].split(".",1) + def dchar_not_prefix(self, s): + dur = s[0].split(".", 1) dots = 0 - if len(dur)>1: - dots = len(dur[1])+1 - return [dur[0],dots] + if len(dur) > 1: + dots = len(dur[1]) + 1 + return [dur[0], dots] - def escaped_decimal(self,s): + def escaped_decimal(self, s): val = s[0] - val["text"] = "<"+val["text"]+">" + val["text"] = "<" + val["text"] + ">" return val - def random_pitch(self,s): + def random_pitch(self, s): return RandomPitch(text="?") - def random_percent(self,s): + def random_percent(self, s): return RandomPercent(text="%") - def duration_chars(self,s): + 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 {"dur": sum(durations), "text": characters} - def dotted_dur(self,s): + def dotted_dur(self, s): key = s[0] val = default_durs[key] - dots = len(s)-1 - if(dots>0): - val = val * (2.0-(1.0/(2*dots))) - return [key+"."*dots,val] + dots = len(s) - 1 + if dots > 0: + val = val * (2.0 - (1.0 / (2 * dots))) + return [key + "." * dots, val] - def decimal(self,s): + def decimal(self, s): val = s[0] - return {"dur": float(val),"text": val.value} + return {"dur": float(val), "text": val.value} - def dot(self,s): + def dot(self, s): return "." - def dchar(self,s): + def dchar(self, s): chardur = s[0].value return chardur - def WS(self,s): + def WS(self, s): return Whitespace(text=s[0]) - def subdivision(self,items): + def subdivision(self, items): values = flatten(items[0]) - return Subdivision(values=values,text="["+"".join([val.text for val in values])+"]") + return Subdivision( + values=values, text="[" + "".join([val.text for val in values]) + "]" + ) - def subitems(self,s): + def subitems(self, s): return s # Eval rules - def eval(self,s): + def eval(self, s): val = s[0] return Eval(values=val) - def operation(self,s): + def operation(self, s): return s - def atom(self,s): + def atom(self, s): val = s[0].value - return Atom(value=val,text=val) + return Atom(value=val, text=val) # List rules - def list(self,items): - if len(items)>1: + def list(self, items): + if len(items) > 1: prefixes = sum_dict(items[0:-1]) values = items[-1] - seq = ListSequence(values=values,wrap_start=prefixes["text"]+"(") + seq = ListSequence(values=values, wrap_start=prefixes["text"] + "(") seq.update_values(prefixes) return seq else: seq = ListSequence(values=items[0]) return seq - def repeated_list(self,items): - if len(items)>2: - prefixes = sum_dict(items[0:-2]) # If there are prefixes - if items[-1]!=None: - seq = RepeatedListSequence(values=items[-2],repeats=items[-1],wrap_end=":"+items[-1].text+")") + def repeated_list(self, items): + if len(items) > 2: + prefixes = sum_dict(items[0:-2]) # If there are prefixes + if items[-1] != None: + seq = RepeatedListSequence( + values=items[-2], + repeats=items[-1], + wrap_end=":" + items[-1].text + ")", + ) else: - seq = RepeatedListSequence(values=items[-2],repeats=Integer(text='1', value=1)) + seq = RepeatedListSequence( + values=items[-2], repeats=Integer(text="1", value=1) + ) seq.update_values(prefixes) return seq else: - if items[-1]!=None: - seq = RepeatedListSequence(values=items[-2],repeats=items[-1],wrap_end=":"+items[-1].text+")") + if items[-1] != None: + seq = RepeatedListSequence( + values=items[-2], + repeats=items[-1], + wrap_end=":" + items[-1].text + ")", + ) else: - seq = RepeatedListSequence(values=items[-2],repeats=Integer(text='1', value=1)) + seq = RepeatedListSequence( + values=items[-2], repeats=Integer(text="1", value=1) + ) return seq - + def SIGNED_NUMBER(self, s): val = s.value - return Integer(text=val,value=int(val)) + return Integer(text=val, value=int(val)) - def number(self,s): + def number(self, s): return s - def cyclic_number(self,s): + def cyclic_number(self, s): return Cyclic(values=s) - def lisp_operation(self,s): + 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])+")") + return Operation( + operator=op, + values=values, + text="(+" + "".join([v.text for v in values]) + ")", + ) - def operator(self,s): + def operator(self, s): val = s[0].value return Operator(text=val) - def list_items(self,s): + def list_items(self, s): return Sequence(values=s) - def list_op(self,s): + def list_op(self, s): return ListOperation(values=s) - def euclid(self,s): + def euclid(self, s): params = s[1][1:-1].split(",") - init = {"onset":s[0],"pulses":params[0],"length":params[1]} - text = s[0].text+s[1] - if len(params)>2: + init = {"onset": s[0], "pulses": params[0], "length": params[1]} + text = s[0].text + s[1] + if len(params) > 2: init["rotate"] = params[2] - if len(s)>2: + if len(s) > 2: init["offset"] = s[2] - text = text+s[2].text + text = text + s[2].text init["text"] = text return Euclid(**init) - def euclid_operator(self,s): + def euclid_operator(self, s): return s.value - def repeat(self,s): - if s[-1]!=None: - return RepeatedSequence(values=s[0],repeats=s[-1],wrap_end=":"+s[-1].text+"]") + def repeat(self, s): + if s[-1] != None: + return RepeatedSequence( + values=s[0], repeats=s[-1], wrap_end=":" + s[-1].text + "]" + ) else: - return RepeatedSequence(values=s[0],repeats=Integer(value=1,text='1')) + return RepeatedSequence(values=s[0], repeats=Integer(value=1, text="1")) diff --git a/ziffers/parser.py b/ziffers/parser.py index 83f801a..eea3060 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -6,7 +6,14 @@ from lark import Lark grammar_path = Path(__file__).parent grammar = grammar_path / "ziffers.lark" -ziffers_parser = Lark.open(grammar, rel_to=__file__, start='root', 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 + return ziffers_parser.parse(expr) diff --git a/ziffers/scale.py b/ziffers/scale.py index 7c5201e..206a9a0 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1495,15 +1495,7 @@ SCALES = { "Chromatic": 111111111111, } -note_to_interval = { - "C": 0, - "D": 2, - "E": 4, - "F": 5, - "G": 7, - "A": 9, - "B": 11 -} +note_to_interval = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} modifiers = { "#": 1, @@ -1511,10 +1503,11 @@ modifiers = { "s": 1, } + def note_to_midi(name: str) -> int: - ''' Parse note name to midi ''' - items = re.match(r"^([a-gA-G])([#bs])?([1-9])?$",name) - if items==None: + """Parse note name to midi""" + items = re.match(r"^([a-gA-G])([#bs])?([1-9])?$", name) + if items == None: return 60 values = items.groups() octave = int(values[2]) if values[2] else 4 @@ -1522,31 +1515,30 @@ def note_to_midi(name: str) -> int: interval = note_to_interval[values[0].capitalize()] return 12 + octave * 12 + interval + modifier + def get_scale(name: str) -> list: """Get a scale from the global scale list""" - scale = SCALES.get( - name.lower().capitalize(), - SCALES["Chromatic"]) + scale = SCALES.get(name.lower().capitalize(), SCALES["Chromatic"]) return list(map(int, str(scale))) def note_from_pc( - root: int | str, - pitch_class: int, - intervals: str | list[int | float], - cents: bool = False, - octave: int = 0, - modifier: int = 0) -> int: + root: int | str, + pitch_class: int, + intervals: str | list[int | float], + cents: bool = False, + octave: int = 0, + modifier: int = 0, +) -> int: """Resolve a pitch class into a note from a scale""" # Initialization root = note_to_midi(root) if isinstance(root, str) else root - intervals = get_scale(intervals) if isinstance( - intervals, str) else intervals + intervals = get_scale(intervals) if isinstance(intervals, str) else intervals intervals = list(map(lambda x: x / 100), intervals) if cents else intervals # Computing the result - interval_sum = sum(intervals[0:pitch_class % len(intervals)]) + interval_sum = sum(intervals[0 : pitch_class % len(intervals)]) - note = (root + interval_sum if pitch_class >= 0 else root - interval_sum) - return note + octave*sum(intervals) + modifier + note = root + interval_sum if pitch_class >= 0 else root - interval_sum + return note + octave * sum(intervals) + modifier