From 1acddb069f551aa3dda9c07476b116aa0d526a4c Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Tue, 31 Jan 2023 23:00:31 +0200 Subject: [PATCH 01/44] Trying out larl parser --- LICENSE.txt | 2 - pyproject.toml | 4 +- ziffers/__init__.py | 4 + .../__pycache__/ZiffersData.cpython-311.pyc | Bin 0 -> 4933 bytes .../ZiffersTransformer.cpython-311.pyc | Bin 0 -> 8279 bytes ziffers/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 311 bytes ziffers/__pycache__/classes.cpython-311.pyc | Bin 0 -> 4954 bytes ziffers/__pycache__/common.cpython-311.pyc | Bin 0 -> 603 bytes ziffers/__pycache__/defaults.cpython-311.pyc | Bin 0 -> 1274 bytes ziffers/__pycache__/mapper.cpython-311.pyc | Bin 0 -> 8266 bytes ziffers/__pycache__/parser.cpython-311.pyc | Bin 0 -> 1025 bytes .../__pycache__/transformer.cpython-311.pyc | Bin 0 -> 8285 bytes ziffers/classes.py | 68 +++++++++++++ ziffers/common.py | 2 + ziffers/defaults.py | 37 +++++++ ziffers/ebnf.py | 40 -------- ziffers/mapper.py | 90 ++++++++++++++++++ ziffers/parser.py | 68 ++++--------- ziffers/ziffers.lark | 23 +++++ 19 files changed, 242 insertions(+), 96 deletions(-) create mode 100644 ziffers/__pycache__/ZiffersData.cpython-311.pyc create mode 100644 ziffers/__pycache__/ZiffersTransformer.cpython-311.pyc create mode 100644 ziffers/__pycache__/__init__.cpython-311.pyc create mode 100644 ziffers/__pycache__/classes.cpython-311.pyc create mode 100644 ziffers/__pycache__/common.cpython-311.pyc create mode 100644 ziffers/__pycache__/defaults.cpython-311.pyc create mode 100644 ziffers/__pycache__/mapper.cpython-311.pyc create mode 100644 ziffers/__pycache__/parser.cpython-311.pyc create mode 100644 ziffers/__pycache__/transformer.cpython-311.pyc create mode 100644 ziffers/classes.py create mode 100644 ziffers/common.py create mode 100644 ziffers/defaults.py delete mode 100644 ziffers/ebnf.py create mode 100644 ziffers/mapper.py create mode 100644 ziffers/ziffers.lark diff --git a/LICENSE.txt b/LICENSE.txt index 6bbcd4e..b3dc361 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,3 @@ MIT License -Copyright (c) [2022] [Raphaël Forment] - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pyproject.toml b/pyproject.toml index 83e72ab..40a5ec2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "Port of the Ziffers numerical notation for Python" authors = [ {name = "Raphaël Forment", email="raphael.forment@gmail.com"}, - {email = "raphael.forment@gmail.com"} + {name = "Miika Alonen" email = "amiika@gmail.com"} ] license = {file = "LICENSE.txt"} readme = "README.md" @@ -17,7 +17,7 @@ classifiers = [ ] dependencies = [ - "parsimonious>=0.10.0", + "lark>=1.1.5", "rich>=12.6.0" ] diff --git a/ziffers/__init__.py b/ziffers/__init__.py index bd753cc..ebecbbc 100644 --- a/ziffers/__init__.py +++ b/ziffers/__init__.py @@ -1 +1,5 @@ from .parser import * +from .mapper import * +from .classes import * +from .common import * +from .defaults import * diff --git a/ziffers/__pycache__/ZiffersData.cpython-311.pyc b/ziffers/__pycache__/ZiffersData.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95e428b8893defee120eb417289bfe1c1b7cdfc2 GIT binary patch literal 4933 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 literal 0 HcmV?d00001 diff --git a/ziffers/__pycache__/__init__.cpython-311.pyc b/ziffers/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b976db42000e000500ba8ca120b7da4c10b5d22 GIT binary patch literal 311 zcmZ3^%ge<81m^WOlgohgV-N=hn4pZ$dO*f>h7^Vr#vFza2+atknV>W?lx6|aOu-DA z%r6;%nlu@2F>3i~vfg4VNGvK&ExN^)n^;f)q}h{m5{rvdi*K4BO~Jt2GI*Jgvu7XzyL+4;^G$=P*4#l Fya1dWR^R{t literal 0 HcmV?d00001 diff --git a/ziffers/__pycache__/classes.cpython-311.pyc b/ziffers/__pycache__/classes.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cda06edfc2a17c995b7f9136deafa43bf491bf6a GIT binary patch literal 4954 zcmb_f%Wu?17$2{_n|)?UvUvc3LV?1zB|>VY5)`Fs2`vwGsTz>l6_w@M29|DigYD%J zrII6YShbN#PxR0OCt5`Rn505#>Jw5=y#<96r+&W~ui3Rt7ST@Z-;QT~-+Ygm`DXHK zp^#-%esKTNFDQ)tjgL-A`IW7u1Y`G@!3?R+7Nn}Qkf##^)5Oj6;ii0Umbf`HwXF>UJ~vO? zLLYA0=MECL*oT|(xjTqE)Q6k(xx>UA**3a4pIIX2XrCzLeeM`>$NO*#K6iq+JI(yI z5gYWmyNFxpGj1nhyNNs5hg&rE%(L>|f3Q@_2`^)4j;7Z&+xAkLZPau}_g4}B>8z=( zUG#a6nXD=qteUvUOxaWnX@**j1dS(q%4epdd5UA+aw_GdmsZt=wqUBNmsQn;rqQaS zo>$fDEv@eN$hKp7MOD=rji#eHwPwRs)rV{gnxB85pSX00yj*W;j+IA40l+mDQ5)*O09B%PW(M7}?%5T3-Lpap0b&k~H%w1%>Q3ch#7eo=h_`VRHUe_-Hs;;a?&%eJ-P6KGaN}*eWP~+j4q0q>RopyuQn~C z;>nA;T^i;jy{fxuE;0Fv0D(xE>}ht^AnAD0fdmDNm}{`kcAc%qOD_v9>9=x!+8r{8G7 z?Qof03+$g`y}mu5o<~71(rU%)4H=XmcWhxb-(y#3xUVzcNz2l;K-*7=)$F{;JY|W8 zg5r~yI_AkV5jJAJPlV%nb_N?mZ!Rp(E*T#xm21sf!zvTkdKXeJX2L>{mr8LukAjI6>avaE(2qC2zbVH3ZSMDT zR9hcE{+O^h{kS;2mYx=>^5JcNfaLjsA~p@C$5U`a;_(pM365|(!b$oFIsn*u_E!XM z*WZ);j=Q6)=hl>Qde)Wkjogqsx3Qz-&b1G$DJ6Q=mC|NHN*>u{0Vq=P1U|zcQV4YE z^acg9jIsp_`tL{o8ke3oueZ#GZt{><9!3t~P*g&R9GlPr9D`=$lkMCV{tNpd^gr02 zn{Rz%)Na&l%7(%PYnnz5PtOe60PmxMGgMfCrg$s*Bx0F1n&PES-q!0i-NFX(k~)2u zm1s8p5b7oXaVN! zS`ji~XgiQ9LaNv$Rov`I6(ALc_60~4AXVs+Dg;tvkP1V44pL)~8tal8^QB~|w8^5- zKJ^r>&j5iGXj$K{5$FoT-9ykd0$n446liH*Y7$amfG%o<)MS^`WZ)NBD8bAgXty9m z77}zxm3*lIi4t}co}%p;AdmrR*tc&2O2Qy!YXX`k0x8fsvlSNLgWNp``?iq^2M&W36fk5!!jUmHC&_wW&==VDMOJ{2`i8R1vLyQjFK=8 zx;}Qe1OvL6DNNZcAlV{uG}D+;7+RTdnV83z&QJx^mB&=Wkj@D5o8L>2DK9~8)nvKF zQc_uvdW$6|v$*6IdsAmI?j3*RMG3&JNvEQn^A$g<57L6T&n{+P7xy?;qki0^1h0+S8 zCFvLC^sdP1U69i|;B=AO{Q?jrr(H-&zrd3I8E7(tpC(fgC;-70u4E|U0kOG(#4iq; p-29Z%oK(9aJ|GwD7DFK812ZEd;|C^2#tg;@E*}^`G=36nBmf3Kgctw- literal 0 HcmV?d00001 diff --git a/ziffers/__pycache__/defaults.cpython-311.pyc b/ziffers/__pycache__/defaults.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c237efa5e17b4122f2fd3c7db4d13f2fca4a19ff GIT binary patch literal 1274 zcmX|;$!il~7{+IsrcK+d-S;(J(j{HerfIq)-934cf=EC@-)uH*nvL3=`X>a7D0uNw zRPf+M@ZiCV=)r>*p#%>_5j=SKpa{~F&zt!sU&xc+`_Au~Fw6^!rCz~#l1) zJm{xd!u1cp6D6ULLh89nRb)U`lMz`%wMY}yA?v9DX(kKON;agO9LPp$LN-$ivX$D9 z?bLznq%LGP^&oqx57|!x=PLeZkcKchX&5;|qsTFGA;)O~IZ0DUH%%jFXcjp~9^^bN zAicDR^wAR1Ps_*^T1Bo=02!opqqufU>S0WK=Kf$wJ&5g< zPYXCH_x50+irjKA;li<$j9`*`1=5Kw{R@=TeJT9~pGZjr3Wm5_xBdhQRdIdz4eeA` zeu3k%a)nB|Qu&TbTCZF}JCc=e;IyoKMI}halC4zUqmtDt z@1R9xqUl1vn=w&mrY?=_xM6r1Th^mr@b@K=K(l6?2qN z8eEWjMY-)#OJ<5h!}(?fo!sH!TeoObJ_)lu{a1Lz9m7eEM6Y}Fhavt4r>%#E7U#N4ddnu~j)g}IgeEz!o@ zuK69zotodp+|B-7(Zk%!{ mRJCDUUJbcmHNb+EBfSzw&&cLUGMCU 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 literal 0 HcmV?d00001 diff --git a/ziffers/__pycache__/parser.cpython-311.pyc b/ziffers/__pycache__/parser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d3b26f0a82683cb8ef0bfd9cd6e88355b7b7ed4 GIT binary patch literal 1025 zcmY*W&1(}u6rW8dn`Ad1Oxm>n zKY0*QCUnkzfwPNrqzfjp1Vs?IpD+_E+$PPWg%up+u$i(%MHEm15lqqt0z!u%Lt2me zAtg()da5HMq)1RHu7fuuB9XK%>S@U44j^?H9iY#dgA~LdLsIg3_6<@Bq^L`+1kcej z+!9EBeuJe8bMgmvmorvoiZJUllKmK#>1gI|hAx|F5(?|03KJLjgeR;;m zGiPvuSVmPX&9GIqlU==0UcFhqwYIiYDKSiqdXw_;nP_Q_14~Lfux8VELitDYjnWns z!yXO8B&r(hlh_{1J*ApuX%wdQnk*eg47VL(vr%REK`Z}uP7BZ{0L=l}ngz{h5ptgp z9pgtOP=7Lat+u{ll6&+rgnVW0H&CZZK@bLL!jJO+jr$P~&_zGa19ZWU^GR`R+uM2Y zcI(|%zj&pa8%Ps-Q$6WgH+@pNv@`yGa&PhD?JtX87ku>@a8Gsns_U1$Udij1yl&wX z3&Pb?bgo2%bK9MxxM(0>7q CJqHB< literal 0 HcmV?d00001 diff --git a/ziffers/__pycache__/transformer.cpython-311.pyc b/ziffers/__pycache__/transformer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc3dc3f64432b31af5f58305bd38fbdb55d02d74 GIT binary patch literal 8285 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 C?f#np literal 0 HcmV?d00001 diff --git a/ziffers/classes.py b/ziffers/classes.py new file mode 100644 index 0000000..7d82b78 --- /dev/null +++ b/ziffers/classes.py @@ -0,0 +1,68 @@ +from dataclasses import dataclass, asdict + +@dataclass +class Meta: + text: str + +@dataclass +class Duration(Meta): + dur: float + +@dataclass +class Octave(Meta): + oct: int + +@dataclass +class Event(Meta): + dur: float = None + +@dataclass +class Pitch(Event): + pc: int = None + dur: float = None + oct: int = None + +@dataclass +class RandomPitch(Event): + pc: int = None + + +@dataclass +class Chord(Event): + pcs: list[Pitch] = None + +@dataclass +class Function(Event): + 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): + return "".join([val.text for val in self.values]) + +@dataclass +class Sequence(Meta): + values: list[Event] + +@dataclass +class Subdivision(Meta): + values: list[Event] + +@dataclass +class Cyclic(Sequence): + cycle: int = 0 + +@dataclass +class RandomPitch(Meta): + min: int + max: int + +@dataclass +class Range(Meta): + start: int + end: int \ No newline at end of file diff --git a/ziffers/common.py b/ziffers/common.py new file mode 100644 index 0000000..3996faf --- /dev/null +++ b/ziffers/common.py @@ -0,0 +1,2 @@ +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 diff --git a/ziffers/defaults.py b/ziffers/defaults.py new file mode 100644 index 0000000..56f598b --- /dev/null +++ b/ziffers/defaults.py @@ -0,0 +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 + } diff --git a/ziffers/ebnf.py b/ziffers/ebnf.py deleted file mode 100644 index f2a67a3..0000000 --- a/ziffers/ebnf.py +++ /dev/null @@ -1,40 +0,0 @@ - -ebnf = r""" - ziffers = (bar / octup / octdown / subdiv / escape / rhythm / float / chord / pc / ws?)+ - - escape = (lt (chord / pc) gt) - subdiv = (lbra ziffers rbra) - - chord = pc{2,} - pc = (neg_pc / pc_basic) - neg_pc = (~r"-" pc) - pc_basic = ~r"[0-9TE]" - - rhythm = ~r"[mklpdcwyhnqaefsxtgujoz]" - - float = ~r"\d+\.\d+" - - lpar = "(" - rpar = ")" - lbra = "[" - rbra = "]" - lcbra = "{" - rcbra = "}" - lt = "<" - gt = ">" - - - octup = "^" - octdown = "_" - - bar = "|" - - plus = "+" - minus = "-" - times = "*" - div = "/" - - emptyline = ws+ - comma = "," - ws = ~"\s*" - """ diff --git a/ziffers/mapper.py b/ziffers/mapper.py new file mode 100644 index 0000000..21f921b --- /dev/null +++ b/ziffers/mapper.py @@ -0,0 +1,90 @@ +from lark import Lark, Transformer +from classes import * +from common import flatten +from defaults import default_durs +from collections import Counter + +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 randompitch(self,s): + val = s[0][1:-1].split(",") + return RandomPitch(min=val[0],max=val[1],text=s[0]) + + def range(self,s): + val = s[0].split("..") + return Range(start=val[0],end=val[1],text=s[0]) + + 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])+">") + + def pc(self, s): + if(len(s)>1): + counter = Counter() + for d in s: + counter.update(d) + result = dict(counter) + result["text"] = result["text"][::-1] + return Pitch(**result) + else: + val = s[0] + return Pitch(**val) + + def pitch(self,s): + return {"pc":int(s[0].value),"text":s[0].value} + + def prefix(self,s): + return s[0] + + def oct_change(self,s): + octave = s[0] + return [Octave(oct=octave["oct"],text=octave["text"]),s[1]] + + 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 dur_change(self,s): + duration = s[0] + return [Duration(dur=duration["dur"], text=duration["text"]),s[1]] + + def duration(self,s): + durations = [val[1] for val in s] + characters = "".join([val[0] for val in s]) + return {"dur": sum(durations), "text":characters[::-1]} + + def dur(self,s): + key = s[0] + val = default_durs[key] + dots = len(s)-1 + if(dots>1): + val = val * (2.0-(1.0/(2*dots))) + return [key+"."*dots,val] + + def dot(self,s): + return "." + + def dchar(self,s): + chardur = s[0].value + return chardur + + def WS(self,s): + return Meta(text=s[0]) + + def subdivision(self,items): + values = flatten(items[0]) + return Subdivision(values=values,text="["+"".join([val.text for val in values])+"]") + + def subitems(self,s): + return s \ No newline at end of file diff --git a/ziffers/parser.py b/ziffers/parser.py index d790119..74aad8e 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -1,57 +1,21 @@ -from parsimonious.grammar import Grammar -from parsimonious.nodes import NodeVisitor from rich import print -from .ebnf import ebnf +from mapper import * +from pathlib import Path +from lark import Lark -__all__ = ('ZiffersVisitor', 'parse_expression',) +grammar_path = Path(__file__).parent +grammar = grammar_path / "ziffers.lark" -GRAMMAR = Grammar(ebnf) +ziffers_parser = Lark.open(grammar, rel_to=__file__, start='value', parser='lalr', transformer=ZiffersTransformer()) -class ZiffersVisitor(NodeVisitor): - - """ - Visitor for the Ziffers syntax. - """ - - def visit_ziffers(self, node, children): - """ - Top-level visiter. Will traverse and build something out of a complete and valid - Ziffers expression. - """ - print(f"Node: {node}, Children: {children}") - result = {'ziffers': []} - - for i in children: - if i[0] in (None, [], {}) and isinstance(i[0], dict): - continue - try: - result['ziffers'].append(i[0]) - except Exception as e: - print(f"[red]Error in ziffers:[/red] {e}") - pass - return result - - def visit_pc(self, node, children): - return {node.expr_name: node.text} - - def visit_escape(self, node, children): - return {node.expr_name: node.text} - - # def visit_subdiv(self, node, visited_children): - #  key, values = visited_children - #  ret)rn {key, dict(values)} - - def generic_visit(self, node, children): - """ - This method seem to be the generic method to include in any NodeVisitor. - Probably better to keep it as is for the moment. This is appending a whole - lot of garbage to the final expression because I don't really know how to - write it properly... - """ - return children or node - -def parse_expression(expression: str) -> dict: - tree = GRAMMAR.parse(expression) - visitor = ZiffersVisitor() - return visitor.visit(tree) +def parse_expression(expr): + return ziffers_parser.parse(expr) +if __name__ == '__main__': + print(ziffers_parser.parse("[1 [2 3]]")) + #print(ziffers_parser.parse("(1 (1,3) 1..3)")) + #print(ziffers_parser.parse("_^ q _qe^3 qww_4 _123 <1 2>")) + #print(ziffers_parser.parse("q _2 _ 3 ^ 343")) + #print(ziffers_parser.parse("2 qe2 e4").values) + #print(ziffers_parser.parse("q 2 <3 343>")) + #print(ziffers_parser.parse("q (2 <3 343 (3 4)>)")) diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark new file mode 100644 index 0000000..6fcd6d5 --- /dev/null +++ b/ziffers/ziffers.lark @@ -0,0 +1,23 @@ + + ?value: root + root: (pc | dur_change | oct_change | WS | chord | cycle | randompitch | range | list | subdivision)* + list: "(" root ")" + randompitch: /[\(][-?0-9][,][-?0-9][\)]/ + range: /[-?0-9]\.\.[-?0-9]/ + cycle: "<" root ">" + pc: prefix* pitch + pitch: /[-?0-9TE]/ + prefix: octave | duration + oct_change: octave WS + octave: /[_^]+/ + chord: pc pc+ + dur_change: duration WS + duration: dur+ + dur: dchar dot* + dchar: /[mklpdcwyhnqaefsxtgujzo]/ + dot: "." + subitems: (pc | WS | chord | cycle | subdivision)* + subdivision: "[" subitems "]" + + %import common.SIGNED_NUMBER + %import common.WS \ No newline at end of file From a01234968d712bbc66d8383f7f4911938bb85e5c Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Tue, 31 Jan 2023 23:38:16 +0200 Subject: [PATCH 02/44] Testing tests --- tests/__init__.py | 1 + tests/run_tests.py | 13 +++++++++++++ ziffers/__pycache__/__init__.cpython-311.pyc | Bin 311 -> 311 bytes ziffers/__pycache__/parser.cpython-311.pyc | Bin 1025 -> 1025 bytes 4 files changed, 14 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/run_tests.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a4e4a35 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +import ziffers \ No newline at end of file diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..b69c9aa --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,13 @@ +import pytest +from ziffers import * + +@pytest.mark.parametrize( + "pattern", + [ + "1 2 3", + "(1 (2 3))" + "q 2 q2 eq.4" + ], +) +def test_list_arithmetic(pattern: str): + assert parse_expression(pattern).text == pattern \ No newline at end of file diff --git a/ziffers/__pycache__/__init__.cpython-311.pyc b/ziffers/__pycache__/__init__.cpython-311.pyc index 3b976db42000e000500ba8ca120b7da4c10b5d22..1de4bd2664f91eedaa13e9489207ad5ab560a2eb 100644 GIT binary patch delta 19 Zcmdnaw4I4-IWI340}#A!*~q2K2mmnA1fT!_ delta 19 Zcmdnaw4I4-IWI340}z Date: Tue, 31 Jan 2023 23:03:35 +0100 Subject: [PATCH 03/44] Correcting some imports. By doing so, I have also broken the __main__.py that was living in parser.py but repl.py is now usable :) The old tests should probably be moved in the tests folder. --- ziffers/__pycache__/__init__.cpython-311.pyc | Bin 311 -> 301 bytes ziffers/__pycache__/classes.cpython-311.pyc | Bin 4954 -> 4944 bytes ziffers/__pycache__/common.cpython-311.pyc | Bin 603 -> 592 bytes ziffers/__pycache__/defaults.cpython-311.pyc | Bin 1274 -> 1264 bytes ziffers/__pycache__/mapper.cpython-311.pyc | Bin 8266 -> 8229 bytes ziffers/__pycache__/parser.cpython-311.pyc | Bin 1025 -> 1016 bytes ziffers/mapper.py | 10 +++++----- ziffers/parser.py | 2 +- 8 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ziffers/__pycache__/__init__.cpython-311.pyc b/ziffers/__pycache__/__init__.cpython-311.pyc index 1de4bd2664f91eedaa13e9489207ad5ab560a2eb..542df8fc81b45e4205377aa8e5f0600bf090eca2 100644 GIT binary patch delta 61 zcmdnaw3dl?IWI340}v$i+)OT<$lI%;ryp9JT2!o`RGO5pUzM4b2IS}#RF-7q=fSx8 I6Hnv=08;N2g#Z8m delta 71 zcmZ3>w4I4}IWI340}#A!xtUxxk+)aRBH1b?v^ce>I3_nUGdoc)F(*GSH7}+rGc64$ Rs9R83l93PR#!TFo4*+Qg8lwOJ diff --git a/ziffers/__pycache__/classes.cpython-311.pyc b/ziffers/__pycache__/classes.cpython-311.pyc index cda06edfc2a17c995b7f9136deafa43bf491bf6a..c937db542cdc1bf939c410a56984439aed55d50a 100644 GIT binary patch delta 64 zcmcbmc0rAIIWI340}$NkzL_kzk=Ke@MOQzxIJKx)KdCe+U%x6dEe*)gEvPKX$j^gu L^*5I?KjjAiq=gnx delta 74 zcmcbhc1w+SIWI340}ymK+)NJH$ZN%{XP#^o6Iz^FR2-9=nVFramza~Emzoz-m6?_X U6x1!KEXl}+b7MAVGe6}A0Hcc<(EtDd diff --git a/ziffers/__pycache__/common.cpython-311.pyc b/ziffers/__pycache__/common.cpython-311.pyc index 8232b4efe2bc94f23fdc14e6054ac9c34ea5eb6f..b26c5e59c25ffbb245096dec2eac038f42664781 100644 GIT binary patch delta 100 zcmcc3a)E_+IWI340}$NkzM0&#k=Kb)MMpohIJKx)KdCe+U%x6dEe*)gEvPKX$j^gu t^(WUdN=SwRwKXu@_4dCI5PZ_|jOEpcma+`&BIWI340}w1#x{=(xk=Kc_-YnTFCbT%Us5mA!Gc!9;FEJ-SFEuZwDl;t& zD5zUdS(1?t=f))G=jP_;=@nEKaR5yL6O2GcaTt(jV7Tk;e<2|Fq~#gQs}V65ykjnK rr}vsqFrH92#jMl5$9}8whUASJTQqhAZqm6R=QcNeLGp^phZ%bT*cdnc diff --git a/ziffers/__pycache__/defaults.cpython-311.pyc b/ziffers/__pycache__/defaults.cpython-311.pyc index c237efa5e17b4122f2fd3c7db4d13f2fca4a19ff..b40e672ec49f1907e12fb94fcbb4ab66af29a6da 100644 GIT binary patch delta 64 zcmeyx`GJ#nIWI340}$NkzL{LMk@o_Vik^OGacWVqeo|>tzJ66^S{jg}TToe&k)H?S L>Tl*|e$NO1(C8N9 delta 74 zcmeys`HPcxIWI340}xD7xsg13Bku(!J&Rf&oRWTVm)zDBYrs{eiQsMilj6 z3=$7D69eHSF+v4R&^_}9Xrk%K)FvK15M%Vh!GnqOww9!mx1V|M``)~rnca^=uZH9; zS+*fIY?-fN?wRa*5M;qB(vXH@D2{^|BaPGe7#Dz9i1BeDD43cU6XQ})Vm{KOSVi0t zv|z-cb4aUrgfvS8Z}JH3u%|eJR?Vs+#kLQjf(!Nl2_^LTB#B4JJ}zpwS9!Os;;ugL zlpZ0vcP>4b^e#r{=UL!cxRbh`Oq4>eahx0GIU*QIk>DNy8t?^CPlZUTz(*1qae>5< zUN#lP%b~=z2oUol&JvsW$OeByiR%e1cyE)UXZa(5;BVlPXAgjOrs}STbMKO=qfu;V!d#&M{K7U16>31;4JWDH(sppRGMi zkXl&J0sIUKSK#$@oQ#2AY7R*dbodf(x0_Uhq8cqVJD6?oL+u~?EGTeYM4+GQ8nDOs zUKhn!Fed9y;MQ(Pgc#Umc7Q((zVLtVbi=7gVWuoL(G9+io^o1@AI=)Kn*MD#CZB~` zu~h4A?38Aq)(N#%EloyPq+g+_#(r+uN4FYPdyFZh0WeNQ*FDDsa}`{ECi>j39RwuN2;i9muL?5TT3T@bpQ`@84<&bhyP?wNVo`LR=a zE=d)Ljl;9wMus0s;oBG3<`L47j?>7(Dz-S4!?5Sl{2;7+TChY_bU7jIvC7mk*B8^} zmRI#+#Gx*vd+s2;%;28!2)$x+Yp4}^c@in!b=WFgQRvmkc+odVv`lU^ODuyZZ( z%$B?>oDbApJrbkud3P&?PYfxO-*P44-FyJ)vm!xG1& zCEP=K|JU07l89R@E_(V#^4gT4@Wcr~#3KPY{hpNdTBZ2Ge0W5eVB2d*?B$YHi5r;LsT1w+!joLmj{>vm;tzD zuN=IF{l21*W`8V!ZW4jkA3CqzWiDnj_n5&3k1;NwI_G~@K$Xsp6;Q<4Z3R^8>^ABg G8u|w}G32BG diff --git a/ziffers/__pycache__/parser.cpython-311.pyc b/ziffers/__pycache__/parser.cpython-311.pyc index cb14bd1b471dc4053942afef6b65415644e1bb15..c59c69ae91df09c4fe4c3ed02c969438d8120c1a 100644 GIT binary patch delta 329 zcmZqV_`%M%oR^o20SK5U+)RGQG?7nRoGFDlhbe?1g&~C{hdGxeiiHuxW}O(I&B!*f zM$(Wyg@cJ9m1h}H(`t}TFj&L6j1|ZSVhD)hP2p-`h~i7-PvM?;OqG#m;v0Dx-e86l zAlBr22~woVc#E~5C^N6*B_ohGS&8wYijID0acWVqeo|>tzJ66^S{jg}TToe&k)H?S z>Q7c=l3^5{Y{wMC!v@rMizO$qD7%P#@-im(cz&QjkuXS{V0OlWaxQE^OeW@dJxUSdvuUTR)URc2ZmP*As^ zvLquP&W)KY!z9BfG})3Vh6iNjEtZ_bqU<8J$qSj>E%|@~MM5A^j`;Yz#N5>Q_#%EF zcO^rSFi1)eNc`fk$<0qG%}KQ@QUr1tfw=hcWJP8fE|CiiP&C<{xt8?<10$pE Date: Tue, 31 Jan 2023 23:09:04 +0100 Subject: [PATCH 04/44] Add pytest to the list of dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 40a5ec2..b498de9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ dependencies = [ "lark>=1.1.5", "rich>=12.6.0" + "pytest=>7.2.1" ] [project.urls] From f48f0bb0acd2bf271e1ebce16e66b91be4476b70 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Tue, 31 Jan 2023 23:43:01 +0100 Subject: [PATCH 05/44] Add a very very simple test checking that the parser actually works. --- pyproject.toml | 7 ++-- tests/__init__.py | 2 +- tests/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 153 bytes .../test_parser.cpython-311-pytest-7.2.1.pyc | Bin 0 -> 1998 bytes tests/run_tests.py | 13 ------- tests/test_parser.py | 33 ++++++++++++++++++ 6 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 tests/__pycache__/__init__.cpython-311.pyc create mode 100644 tests/__pycache__/test_parser.cpython-311-pytest-7.2.1.pyc delete mode 100644 tests/run_tests.py create mode 100644 tests/test_parser.py diff --git a/pyproject.toml b/pyproject.toml index b498de9..d241ae3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "Port of the Ziffers numerical notation for Python" authors = [ {name = "Raphaël Forment", email="raphael.forment@gmail.com"}, - {name = "Miika Alonen" email = "amiika@gmail.com"} + {name = "Miika Alonen", email = "amiika@gmail.com"} ] license = {file = "LICENSE.txt"} readme = "README.md" @@ -18,8 +18,8 @@ classifiers = [ dependencies = [ "lark>=1.1.5", - "rich>=12.6.0" - "pytest=>7.2.1" + "rich>=12.6.0", + "pytest=>7.2.1", ] [project.urls] @@ -31,4 +31,3 @@ repository = "https://github.com/Bubobubobubobubo/ziffers-python" line-length = 88 target_version = ['py310'] include = '\.pyi?$' -exclude = ''' diff --git a/tests/__init__.py b/tests/__init__.py index a4e4a35..8b13789 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -import ziffers \ No newline at end of file + diff --git a/tests/__pycache__/__init__.cpython-311.pyc b/tests/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce987795d8a997c6fe47be0454feb8ddc2071a04 GIT binary patch literal 153 zcmZ3^%ge<81V^XdOlAbqk3k$5011G=XEq=)ogsxGm_d`#ZzV$!6G#vw^GioRv^ce> zSU;&WDPO-TGc66s(JiPf$;i*sFG(#fDb|mV&&nx>^3CT%Lbs5rIP_)tPbBvK+FSxA&aQzfg7cau6=+v}MP zP8J&l)I&g#fKx$$RBW8V@4er= zeQWH^ES$Ipnj||~4 zT8tUo$4C+X@tlha$GA63-8lXtgB0L<{0D@8g4Q7DFI)@O7S}}Yqa~-sCy>MSTnxU+ z|A>C(p8@tf!a3x4QM#i!U~LIa?rBIrJmjy2Fc@d|?oH%)th!?gkxWZG$)G6?`kWj6 z)X4`>oR;{xxLXO<2G>BF!UNRc`Z92X8JtAO2chPGwIwwr`ofS5*%|7cT95AV48Z4+ z;ZsDfIit}Qt;a*%i8O@z^OJs$h|BpNRnCx&*#G2n`D{Le_ay!2A{K+^Aa|@UV(+BX ze=j5cli)&~TCy`9Rso?t->9NZx!<>c_a(drEy+O8>WQKfa}y7<_+bvKOH1xZI*ZfU z+p2=InT)Eq*R^F_$6DQ7R&l-2(DGQz=2SeB#kpB`1S~nMVHGc9HLtqy9Il%=Z06me zI?mx4)kA09u{utNJDgT=UYS+g%OB~4maA*n+_Z^FX}MOti|^laZ|jtr#KE`H_Li#9 zcKN%4Dln(k;r8fI231G@Y=k8hupTundiu zHqq9s+KO&zRlQ=eghusM(}UT_oPP}nJ5NZBurY15Mk>1FQLyx??j=-WY^cYG67L;g z$sWSw;9`t3*^oyoC`~Td1o84T23|NAucoIzf#T8Wm5r6!v|C;UGW0EA{-{=+_UKbz zYJM?D#@=Eh{-sKKwTrzD!MvX)`W^gOZKDn!ACo&M6x$zmE}_wj+aDZWne3pLbi)@r z$qp|kZXBg9wlBT?+iM_T?xzX|sX{wdI2yn8bbWt3doZ5exp$Zv`=u(J@lK*Y&|)MWeAdf5V;3H2?qr literal 0 HcmV?d00001 diff --git a/tests/run_tests.py b/tests/run_tests.py deleted file mode 100644 index b69c9aa..0000000 --- a/tests/run_tests.py +++ /dev/null @@ -1,13 +0,0 @@ -import pytest -from ziffers import * - -@pytest.mark.parametrize( - "pattern", - [ - "1 2 3", - "(1 (2 3))" - "q 2 q2 eq.4" - ], -) -def test_list_arithmetic(pattern: str): - assert parse_expression(pattern).text == pattern \ No newline at end of file diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..8f454a1 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,33 @@ +from ziffers import * + +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)>)", + ] + results = [] + for expression in expressions: + try: + print(f"Parsing expression: {expression}") + result = parse_expression(expression) + results.append(True) + except Exception as e: + print(e) + results.append(False) + + # Return true if all the results are true (succesfully parsed) + print(results) + assert all(results) + +#print(ziffers_parser.parse("[1 [2 3]]")) +#print(ziffers_parser.parse("(1 (1,3) 1..3)")) +#print(ziffers_parser.parse("_^ q _qe^3 qww_4 _123 <1 2>")) +#print(ziffers_parser.parse("q _2 _ 3 ^ 343")) +#print(ziffers_parser.parse("2 qe2 e4").values) +#print(ziffers_parser.parse("q 2 <3 343>")) +#print(ziffers_parser.parse("q (2 <3 343 (3 4)>)")) From 6fea0c275dc8d129f745235b628b177c6b2ec325 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Tue, 31 Jan 2023 23:44:07 +0100 Subject: [PATCH 06/44] Add a gitignore --- .gitignore | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1cb160 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + From 3add48c938499fb84fe75c9fae76229fdb961dd9 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 1 Feb 2023 21:15:09 +0200 Subject: [PATCH 07/44] Added more rules and tests --- .gitignore | 8 +++- main.py | 11 ++--- tests/__pycache__/__init__.cpython-311.pyc | Bin 153 -> 0 bytes .../test_parser.cpython-311-pytest-7.2.1.pyc | Bin 1998 -> 0 bytes tests/test_parser.py | 27 +++++++++--- .../__pycache__/ZiffersData.cpython-311.pyc | Bin 4933 -> 0 bytes .../ZiffersTransformer.cpython-311.pyc | Bin 8279 -> 0 bytes ziffers/__pycache__/__init__.cpython-310.pyc | Bin 162 -> 0 bytes ziffers/__pycache__/__init__.cpython-311.pyc | Bin 301 -> 0 bytes ziffers/__pycache__/classes.cpython-311.pyc | Bin 4944 -> 0 bytes ziffers/__pycache__/common.cpython-311.pyc | Bin 592 -> 0 bytes ziffers/__pycache__/defaults.cpython-311.pyc | Bin 1264 -> 0 bytes ziffers/__pycache__/ebnf.cpython-310.pyc | Bin 856 -> 0 bytes ziffers/__pycache__/example.cpython-310.pyc | Bin 1361 -> 0 bytes ziffers/__pycache__/mapper.cpython-311.pyc | Bin 8229 -> 0 bytes ziffers/__pycache__/parser.cpython-310.pyc | Bin 2070 -> 0 bytes ziffers/__pycache__/parser.cpython-311.pyc | Bin 1016 -> 0 bytes .../__pycache__/transformer.cpython-311.pyc | Bin 8285 -> 0 bytes ziffers/classes.py | 13 ++++-- ziffers/mapper.py | 41 +++++++++++++----- ziffers/parser.py | 11 +---- ziffers/ziffers.lark | 22 ++++++---- 22 files changed, 86 insertions(+), 47 deletions(-) delete mode 100644 tests/__pycache__/__init__.cpython-311.pyc delete mode 100644 tests/__pycache__/test_parser.cpython-311-pytest-7.2.1.pyc delete mode 100644 ziffers/__pycache__/ZiffersData.cpython-311.pyc delete mode 100644 ziffers/__pycache__/ZiffersTransformer.cpython-311.pyc delete mode 100644 ziffers/__pycache__/__init__.cpython-310.pyc delete mode 100644 ziffers/__pycache__/__init__.cpython-311.pyc delete mode 100644 ziffers/__pycache__/classes.cpython-311.pyc delete mode 100644 ziffers/__pycache__/common.cpython-311.pyc delete mode 100644 ziffers/__pycache__/defaults.cpython-311.pyc delete mode 100644 ziffers/__pycache__/ebnf.cpython-310.pyc delete mode 100644 ziffers/__pycache__/example.cpython-310.pyc delete mode 100644 ziffers/__pycache__/mapper.cpython-311.pyc delete mode 100644 ziffers/__pycache__/parser.cpython-310.pyc delete mode 100644 ziffers/__pycache__/parser.cpython-311.pyc delete mode 100644 ziffers/__pycache__/transformer.cpython-311.pyc diff --git a/.gitignore b/.gitignore index b1cb160..1188230 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ # Byte-compiled / optimized / DLL files -__pycache__/ +**/__pycache__/ *.py[cod] *$py.class +# Pytest +.pytest_cache + # C extensions *.so @@ -159,3 +162,6 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +# Debugging file +debug.py + diff --git a/main.py b/main.py index 5f305fa..fa9eba9 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ if __name__ == "__main__": '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", + '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", @@ -32,8 +32,9 @@ if __name__ == "__main__": 'Functions': "(0 1 2 3){x%3==0?x-2:x+2}", 'Polynomials': "(-10..10){(x**3)*(x+1)%12}", } - for expression in expressions: - try: - parse_expression(expression) + for ex in expressions: + try: + print(f"Parsed: "+parse_expression(expressions[ex]).text) except Exception as e: - print(f"[red]Failed on {expression}[/red]: {str(e)[0:40]}...") + print(f"[red]Failed on {ex}[/red]") + #print(f"[red]Failed on {ex}[/red]: {str(e)[0:40]}...") diff --git a/tests/__pycache__/__init__.cpython-311.pyc b/tests/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index ce987795d8a997c6fe47be0454feb8ddc2071a04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 153 zcmZ3^%ge<81V^XdOlAbqk3k$5011G=XEq=)ogsxGm_d`#ZzV$!6G#vw^GioRv^ce> zSU;&WDPO-TGc66s(JiPf$;i*sFG(#fDb|mV&&nx>^3CT%Lbs5rIP_)tPbBvK+FSxA&aQzfg7cau6=+v}MP zP8J&l)I&g#fKx$$RBW8V@4er= zeQWH^ES$Ipnj||~4 zT8tUo$4C+X@tlha$GA63-8lXtgB0L<{0D@8g4Q7DFI)@O7S}}Yqa~-sCy>MSTnxU+ z|A>C(p8@tf!a3x4QM#i!U~LIa?rBIrJmjy2Fc@d|?oH%)th!?gkxWZG$)G6?`kWj6 z)X4`>oR;{xxLXO<2G>BF!UNRc`Z92X8JtAO2chPGwIwwr`ofS5*%|7cT95AV48Z4+ z;ZsDfIit}Qt;a*%i8O@z^OJs$h|BpNRnCx&*#G2n`D{Le_ay!2A{K+^Aa|@UV(+BX ze=j5cli)&~TCy`9Rso?t->9NZx!<>c_a(drEy+O8>WQKfa}y7<_+bvKOH1xZI*ZfU z+p2=InT)Eq*R^F_$6DQ7R&l-2(DGQz=2SeB#kpB`1S~nMVHGc9HLtqy9Il%=Z06me zI?mx4)kA09u{utNJDgT=UYS+g%OB~4maA*n+_Z^FX}MOti|^laZ|jtr#KE`H_Li#9 zcKN%4Dln(k;r8fI231G@Y=k8hupTundiu zHqq9s+KO&zRlQ=eghusM(}UT_oPP}nJ5NZBurY15Mk>1FQLyx??j=-WY^cYG67L;g z$sWSw;9`t3*^oyoC`~Td1o84T23|NAucoIzf#T8Wm5r6!v|C;UGW0EA{-{=+_UKbz zYJM?D#@=Eh{-sKKwTrzD!MvX)`W^gOZKDn!ACo&M6x$zmE}_wj+aDZWne3pLbi)@r z$qp|kZXBg9wlBT?+iM_T?xzX|sX{wdI2yn8bbWt3doZ5exp$Zv`=u(J@lK*Y&|)MWeAdf5V;3H2?qr diff --git a/tests/test_parser.py b/tests/test_parser.py index 8f454a1..074a076 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,4 +1,5 @@ from ziffers import * +import pytest def test_can_parse(): expressions = [ @@ -24,10 +25,22 @@ def test_can_parse(): print(results) assert all(results) -#print(ziffers_parser.parse("[1 [2 3]]")) -#print(ziffers_parser.parse("(1 (1,3) 1..3)")) -#print(ziffers_parser.parse("_^ q _qe^3 qww_4 _123 <1 2>")) -#print(ziffers_parser.parse("q _2 _ 3 ^ 343")) -#print(ziffers_parser.parse("2 qe2 e4").values) -#print(ziffers_parser.parse("q 2 <3 343>")) -#print(ziffers_parser.parse("q (2 <3 343 (3 4)>)")) +@pytest.mark.parametrize( + "pattern", + [ + "1 2 3", + "q3 e4 s5", + ], +) +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]), + ], +) +def test_pcs(pattern: str, expected: list): + assert parse_expression(pattern).pcs == expected \ No newline at end of file diff --git a/ziffers/__pycache__/ZiffersData.cpython-311.pyc b/ziffers/__pycache__/ZiffersData.cpython-311.pyc deleted file mode 100644 index 95e428b8893defee120eb417289bfe1c1b7cdfc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4933 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 diff --git a/ziffers/__pycache__/__init__.cpython-310.pyc b/ziffers/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index f84025ede8d029fa3af714bffde1175d0f9f3bfd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 zcmd1j<>g`k0;Snq$znkIF^Gc(44TX@8G%BYjJFuI z{4^P(*a{Mhic^bLG88cbg~7xxJ^fIiP_ceeX;Qv^Rc2ZmkfU2rS(1^T2jl9;$7kkc cmc+;F6;$5hu*uC&Da}c>1DRY5vYLkh0AcwgumAu6 diff --git a/ziffers/__pycache__/__init__.cpython-311.pyc b/ziffers/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 542df8fc81b45e4205377aa8e5f0600bf090eca2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 301 zcmZ3^%ge<81PMJilM8|LV-N=hn4pZ$dO*f>h7^Vr#vFza2+atknV>W?lx6|aOu-DA z%r6;%nlu@2F>3i~vfg4VNGvK&ExN^)n^;f)q}h{m5{rvdi*KQeEq7-v@{?`x1h2lBR>zu)sK(Q%*!l^kJl@x{Ka9D wo1apelWJGQ1Jn+(yx1K`d|+l|WW2#3dI5$|*YxN7@qa69e>6oaXx@Rp^$>x5|G*hpopp^v>~XbYKqias#a!q14fP=cGrAF zRN6=!tTs~VDLwSSi5AfxlT=7`_k`3_Z;8T*Q{QK1z1X`;jA$pjk7s9|cixYgd1vx# zE|+Fh+CTiI4JnNMjgL-Ac$J-%7-Q>9XS!5lOHx@{ij`v$Go%>%jOnraOqU-K;|aZ@ zJffc`oex%)J*Gm;xFJ81sh3_+0$j!8CWt%Gha2~}N#dsZa1$OkP27x;*wuyskDDcK zt`9fqaR-T;@54=b+&#n{>cdTY++pI5>>AyS$1D(Yv`-YW9(Rnm<9)a}k2^u!y+(G| zhz)w&eZ2r5S3~V>F)ZDxVv6#Z_$Mj$JCo-K474D@%r|x@lEiYUs@x z>RDC2*{syO9@(-@H?OLddc9#+>}sQKsp@041I;f^oV;Qgrgida^J?SdN_BCOK29v( zwXZkoozIipZo`^hzH4G*ae&3vUIM4a z)=Xl=6aZV_jW5<372C|BAqU_Z^QaAVV1O!NJ5z(syfeQ_uQM;CAn=BECN67s<(A>f z4b3h+4p}Kz>(Mrj!bU(Q+QzIi=gh6r>&yuo!Hu@@AoX-F8(pH*r5JHN#+Q!7mrne` zEu(Ii%wZxI6MurLbc^?{e&bLN46g-<8sUnPqP_)r!sb@E% z%L!wET#$te}rmNjAi7>!s=1wA`Jt>y%PuN1g190fhNa3;|P zKqxGj*yHHnu=7bvc1liZwbTxH-Oi6t6h@rO&gE5loy#Hs7%VyfuQh9#FDrGuu_RnI z;Y)b#0v-nT!doO`0UvHRt$(5Hit2hvx z9jql+sZ}XS@z_W0=EKA>0f?CO4ndCx&y+{Ipru zkTuBzvsn*EANH8o!;w2@3LOAA8jgPSngKuBLt z%6(Q&>v6&iKS~nrKv|@FH*u>{YZ_L`M6%Q*f=0hGnbOdev6dl~K>c>7M5I zi0S3@c46Yr!qgvysm;Q%r-frJWi8dBKR)q2o+u>4J(;Hp-3X+Bj0_igGs>7I1M*Y;CvExjNzPf_Bbp!me5Pq{Kp zgoT(N65)7}p25b@8cWOPR*rXK(H+R?nOZ|5p>tH}FsEq%3u)J38{?T#xl^cy}-7FH>LTXTn5~R|-)&Pk@OP>accx=*QdN-xlNkHv9V}s%?&+eoB}> z^E7{EBY8%s$_BUn5t8Qzir6%m9#_G=h{r>0Cpg0G2qx*{=m22rS>F&iU4PHAdmfCg zUD{B_>Dg4q+nFI}w!Np|%(jkfCtGB8a zWkcbDd4@&~PR|V503V`)GgMfCrf4hrBx0F1n&KwT-_>eW&BO+A;~IS!rEoU>2Ma5f$qb}qC&5Xbw=vk?4fyN^;&aQ3|njElRteR1pg0qn|4)jC1tPRSf6 z(O}^GlNyl<-?CEPjj-LR_*cXKYMmRWTgElRjazobv|ZV#>(Lg@!fL?BkrrmWdv(}C zE>tS+aWnpZLWX6^7)}8s0Q}a@uf<4T;y%D*au}aTIegdiJ-`v}{P_ zA(iiv%5Qa~a*zr_>nlj*AeHNq%K1`bkP1R;7E)u78tal8^Q2^{u*JgAy0DGb=Kx;{ zw6y2f2y_ME!4!0jK-Y*b1zOURnuJskpo>}|HQ6OK>H9?%N-(nrT1`lig#=wv1y5=~ zqJ$lWZL~cH_%a|3d-hF0Nf5+rO+eFxF9lj>wt@nDkOxO$-!4+Y;6ZAn_4O+vD0IcJ dD8c|;)cPFYTL_wndsu)Ef|U^|kB$HU diff --git a/ziffers/__pycache__/common.cpython-311.pyc b/ziffers/__pycache__/common.cpython-311.pyc deleted file mode 100644 index b26c5e59c25ffbb245096dec2eac038f42664781..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 592 zcmZ3^%ge<81UI^GCN}};#~=<2FhLogg@BCd3@Hq$3~5X$j4dotj44dP44TZh*osn1 zN{jN68DUz0G#d~z1M%k_Kw>&W36fk5!!jUmHC&_wW&==VDMOJ{2`i8R1vLyQjFK=8 zx;}Qe1OvL6DNNZcAlV{uG}D+;7+RTdnV83z&QJx^mB&=Wkj@D5o8L>2DK9~8)nvKF zQc_uvdW$6|v$*6Idsc~fHW6}0*MBOyWaj60)kIkp0T_d5p%&C zgnG><7*8miV%BNjW4~2-L-IzAEgCxlH|cE9nVY^Kd4=K%r4>p`(l5&CU6IqfAg6c0 z=_0rL1t3aJyO5NAfhGMj&|n5XO{OAHsDbTU$xy@tVsitDUmP~M`6;D2sdhztKrYxR chCs##W=2NF4@``V8H^KLJ}`i2{3O^&0BPcX`Tzg` diff --git a/ziffers/__pycache__/defaults.cpython-311.pyc b/ziffers/__pycache__/defaults.cpython-311.pyc deleted file mode 100644 index b40e672ec49f1907e12fb94fcbb4ab66af29a6da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1264 zcmX|;J4{ny7>3WG6bkgp{eHidOA8drotC?kiy<*C2KahGfzqBGOT(eRFbho;w_s8qUR|Z@Ukyn)Z_i z^OP#N`T=>O?P;Wuex_3iS-?`Vf@M?=+Nc7oq$82h`z0?Qx(*QU~UT}zp!4Vn-edGtnXdIlN060lgV34N4 z8JY#>XdYakMR18iV3?M{6$LIO4_WvKARFk885Xf{)*Q=|CSL+8UCHjPQstvi$*bw9-XA)i9>VrtDfOI zI=Qwj93&{SA6ZJZqORjIK7zuc>$)iEbvrMdo?Jj!V3)8tVTFk@%~g$UIlE1B)#O%1 z1$!m$XGImeU1l8YPMLAByJg11UM({{{VTRDGQ zw6V8yeqD61cXDP%bg_4{Pl_IPjnOMheeC^Q+7JWmgRc*GMVWeJYL03Q#0(CX$l**Y=8|7WNcYZ47MdC8IzXw0NkXzUVB2i zAkSjNOLntc2?|&!rkVtZZMD^;ayU6}gsJnBCs(S}dwYfZmq>Gt%4+@w!E}s8qrSaCKo)OH6QJDN+IR;6}9;~8TSMa}mc7ErDI4T6Ro!oBx z*;;Ih+}4841MH>qdy^~^IPlg;BvV&6y06h#0d@Bccoz1nZ1v5j*fQkNZ$PX9D>z<|8}rcMChw%rdJ7?b=cu| z2qrSIjU3#=7@4F;EMyiqF^e_y4x1&Y&eaganL1h!NEzx`$<0A^UUCr6_=-Y7InT+| za~C)P>|%~B3a_fr)2yGpSw+VOPYQPm`b=ABE{Q~rhvcC@14O!Kp zx+yBix9Z~Lx4~oS1Nn%11&&Sn2KS2Qoo%GE6mE}noXl(nhT|B)Nc0GO>n7Mq%TOiw zX)0!w7Gz_mFiZgbl*nu*c#z<6f+q<+Ot72aX9+eE%o98uZSRWp8N9-@D&@!NqRdfB zA*U-MtV7UAf=3CCjW1w3M`7_}KcF3|xv|sd0Ud{3y+I|DAssq229Xy~!!)hPI^qq< z=AC$!HCdarn4T#mpFPx}eqh0UC)A1U$ntG#VEi@t7n4>EM%xtZ^!!2TmYaZ`5pXvI zjmt+O%3~{yVkp=!jb#jFI+LuB1JPo+?|+xnwAXBJrFCzgH7Q$WU7EKI9bTgWbcQr_ zooX86aLNrhrv|2N7=b<|=*oAFLos2oI>0>+8|o-F+#!eIq*5A8W7(1IeqfyG_A=cx zwx9V8au6=~BGXM{uQFYv+~kB-6*ij2))Y4I;0u*f@Vl!pZxF?Pjy*ZUOmFN03c3S@ z^^G93Mj7@}VZI%QLef;NW4WqtsXnqCF8ORcrgfw&+luFoA5Cn_<=`<6n{r%RdZ|}~ zk%gAyxt14)%Omw)1?qRX->Ymr+SxdGxUsdl^I$uf$>>igVDKBr@T1k<4S5i@`*GiE zKevWMXj=XxI`rJUZS!Np@om2BPm=PgLU4Bge`KpL{7x6l*%V$7Q9aX0HXXz6aWAK diff --git a/ziffers/__pycache__/mapper.cpython-311.pyc b/ziffers/__pycache__/mapper.cpython-311.pyc deleted file mode 100644 index 853b10a83d8b90b07009ab7212384b6d11a36f42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8229 zcmc&(U2GHC6`o&b;slSAI6uG!5}>eN*aXNbq!0pZqHN1{(`6k9F|b+Valqi%$&8bQ z)amlDtqh8`W|b;#6;*D9ShJ+sXtmPjfl55|VIRlUNOebwRH;wAQPHP7v_0p}_-E`P z-Ab!=GWmS}f6hJkJLlf{Y}+;`1&=fJmmmY7zhR{C@MhqJ9|FsiL`ignnxg$QO-YQz zMwn6jRP!+<2_gC+OHq&E-+27BQmv2j zI-gqw$a& zG@3LJ>*Dj`4!|-MqWrW(`5B4+nDVm{1IS4%ATM!%4v7b>ksN>mjugfv06L{wz&gnZ zSTEH9Zj4S-EjBcMlW0&JE%Fu!KK(I1|i49QB_{DKS) z(0owRD3uq-0hXzFVf+w_7o|l}3Hl-AJTB&1V?OE;J>q5b8nZLm0o5VSDWS-u&WGcn zDMi6vJ32NGb)Q#YJl$7kug1FPjlsP>eLH?V7A-`(rvlT{A-QY%w$911SbV;X!q z`c<6@7KZ~V*j*F+zjy^;nYuaqL?0O&$PTe>+tv(mP*JJ;aV8oWuzhS-{(%l2gDFFySILt~8J48w2k zm`?-j9@V->a|cJ&^A~dmFRJ2rP8`?7aa9;6)3E3e>Ne4#6ach{Nk{)HC zO!XsiRy_c%D$^m*sxnPP9V@kG?QNB}vKnZO!_XR;gI=Hg3!Y8-keAijsc;k^aLZT( zM(363NI0(7y%&f|v8i{%@!)k+{5q}3s99Fw*8x95!D%cwzsp8d%JNaQls5tHP8~VJJdqPm zXyOS~II)HJ??4BY;?F=@^&>I=mAKiLikqtvH*bi0p6Ti$A_r0PN<1LPbv6{0OkvBf z!I*R&M13tJAA+z|v^fW0P zW;-8Du63@R`D&jk9?OZxH1U`!9IF;$kq1|x*-9anXk-C&&yK|wSc|^05-V|;B6mTP z#Ru)%w_-sDVZoXz78DEtTL=&tm=GZ9m`Q-IEYRh)tI}S@%SLyT?6og;BxsNBxzzVR0EFm3>a_P_j_rr=TUey z{0GnHv1}~m%K~O&CRz5&%~0MlR~+lNB6~6mxebbc7lYpk?Gy;UZ&W4k)8Km3lQts^ zXpVV6-VJ5FtikAxK@dkcDDS~Eu7b|>+GuP-nGOU)ih}$E%x+RUPo!wO1xOA~OBn}% zEzr=CnptXF+MRUdiC8nkAlsa0K=TYFYd6H^^muk~jrqFf%Nk>--&}y-T;He$c=)__ z_6tbB@~IiiSwc4vGRRkwfqZAf*zu- zv@Wm-#(u9PSZk{^wb&M~!FG)&QAuN5f-AQT;Q3Nueah(z^82AOag@wfBtA?fxCitn zv=40;S0cLwzN#E=ltZh66C`{l@5d1CCu8IAIvWW^Wuz3{F*_{<;;@ONa4@d(#H@H7 zh8nz%1!{tZ&#cpu?vO(YxYLHQL92@TP|yfwIym2Kv)!hHxD%Co0AT3rT*;Zcy^Fo6 z&U<~yzKxdl`{AW<@~wPx%X5m}wueB9&bwQeU7xzrGdXvs=I&ITEa)QYYTlBc>$v zaw5}t!-U&e8>04pL#t=OwBT!JS5Pa+w9)yi zv@76{GNyNej4Pi7unf&HEr%w;3@qCoT@=p7~DcuX!Wg%if2}sbD8&9Qp2s!HNbVicJ?(TmMAbcVli{L zUx=AQ%`;7UJ)UVK>Lw}`5RODgp-C{=+fe^E>GusvB5Y(>a2c(Z&VdBwa=kKa_v`io zg+;J4-fuul%02)z-h;-w#-;aV*k60o-qg9&xol%GgGkk2#B)#uTnSd0osxfu;j%?1 z36>R?eNeo~27XL8~hO+2FtXR252|G;Q)$yH#q4`2=! zMiYi+0bV1ojEE2^WZj$|@BYk2e{}uYT}~4&Ak_6u#mRRNDI85qu8q`;^Hb8rIu%l>$^N9JhKcaNo&|G0%>^89+msGwI#(u* z7@#q7Vj}`j9{eRVF!DHt&EpBN$F&6+DY_HwM<56nyA`Jta?v}#a|;0v;E+ZEz$_DC zW@bQ!IgeNKc$2mH=GNu0Psfb0tyGsI@d7h@W8DLPN7p@C z*MKUX!dEc($#N4_WQ9fOveMfEKL=6^Pom6RE15t3swWok&HU%<;%Py z_GaidITpY{A{JK^oM`E6Ab$YS`4;qExO;*N64#D&kLKEqS0&&q-JM&U zO9k%UN^gsr7(fb#03u18{k&p6~8sz@=&$Mi~NQg|+`fVH=?AGp;Dj@9uCDtW(Q*L$Ay8DT&o?@jtj;i3rliC*U`wQF$lUTg{H zQoIAGE9=|#&bciAJl6gZ0IYc7`ajpOM{C%Fm;XC5`?JBdJzpRA@_;$SIrz;Tzn}pg z9n+4E<+{eyi#Kg>OAY$am4cPlW1l(pa9vS452*U_S zCo=9@8O;(oZ^-ER%MwBm;RA$e1O;Ih;U@?S2*lOE9Z$Z&$UyrP0i-Bp7{qa>`1hQn z`2%p}QUC|T`|0OY843*Fj|nC~v=5^u2rPdI%9o+j$#*=biqJ-5(IPl#-h)vSpt~W` zT@j)EBiOzP0?oe(1552M_m1MR3!66SiRjw9txOm;hbu##|F1 z_X=shh)|#b3seGGaw6CfuM@s>1>~DD?gilKNI+3Sitd1~bW^b?`FaMfw(%)iAqG^h z3&tXmP!K(GgttQKO`+dVp#k~l&@yVWvJCK?rD-}(y`oy*Jk_EW-aNHawZ3_3 fk7|AM)Na-KKIeQi4HLD;^v=JNoPWF|pGf52KA-0} diff --git a/ziffers/__pycache__/parser.cpython-310.pyc b/ziffers/__pycache__/parser.cpython-310.pyc deleted file mode 100644 index 906f8863020697150cb3c94e7ae7eb6d3c55df6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2070 zcmZ`)OK;mo5Z>iWv>vVyxK7P*=wIQrr<`-^q38@LTQcG{j_G}ni+>V{Z@f;XCUQJNp98=GI zKzNP2CxpAgJT|=*z9QWOgB{YLMeTtKk|a=l}AGIA&S}2LQWZLCRTg z6vdq7G7)+hr2{5Mn#n#3SSXV$7P>lh5X2F`P<)QnB2pDzXmsLW=y%wpxy9}cLAMg= z4l8PtD%J`)usgZ=R0;mFy;3 zm*|AlLOLUz-ZVi>=`Yx6L?4qgx_f4LD~PY31_|6=iz9tzs#Q3s-a&!hly|DcyD%-> z%3KzH1hY4&y-6q6uXHI3|JNIJ9aX=FVgtnu6o}rErDkPGL89NZEP*Hfk3mZ`y#v?9} zQ9(LpOEqZ0&mWZR1-*C-S~R2t>U%Ck#v;vvoPn|Pf{;K;q`J#W4`97w9;QgZ4u>)p z7e2ELe1EnL4On(r5N-%lhT#RA4n z-`^hvF;Wm_HQ(nl^nJAsO4TNc4^VuFVnL8{gnTT^E&i)=?KJs@iciwr{t8*C{#}}gSrb`|#o2N1k0} zF&2E!F5&2y>SLT)S_$FR$~CE${4QNaE`A9;$8uk%4y{v#YrCD6w}v{SM5d7(<=sK` zvr~w$ckR*^R-#;?>xBgj9hTKhp+>|Iq7E2-3bP6ZGuKMtpQSXkh5L0Cl5Y{7 S98$Y}37$9*RU@@YZAd8xS2jk7egCQp1rGzwve&+^lSHCSrrXf3c zz{CRw6F7204gLZC3Eu3;66QBCA@L#?vxJ)`U$++7%X{zpe)scz-+s(wWIz&kf2{q9 z0sIP_h?1dk{u_-wFu)K@umn{Qz=#>58Cwcs#1t)}5{!WnH{+J1N&>`y5}G&^0FF@l zv>wd^HAPc~)KCCa87rlAwE9G4nlKVZ5~Jxu3>`%Y_>ww|qmR-jHDkzcLCw;fp==;> z09M2Y@?BG z)G8Iq+*uPn9)GEuo0Jn%H(7~riBVJY>OAgxI`g>X=**>zD<0Erw^C;oWos9!!B2>x zoX?@L2OjE(4ldV4L3jg$S6D=p3#Ay(hN!ggi}BtWfTph;<_5Ehe`m_i5RF@2-CV7g z!-m_=mRGIYkz4Nf&pKOtsAp=C3rM<3{?|;Duoke@>rQXAT4F8txWl3 z>Krq`lbUAfHCxjfspUJx<-5guD=Q17JSVKSR%OAZxn${%gR4n6xLmWg%A_EQdt@6d z5zeU8OsZ+UEmGU#>E}$hES=#Uy(&+H9w&8&+I(2;Kg-hpO|u-%>46Ra*}8?pSn!*C z1~wX`hEe@ZKCtS>rb!>NYv=}xa=)YMih>|?V9X!n4qWjg?!Z-lkS}NZWv9839dGyX zyY2Ve?cDWdx+9P6PqgHl&BSSbe0TIiVSoP9{jc-i=6vl1a!YgDn(ODiR^Dspy=Jyc z1mQ*(E|df$z0)`r3%*$BkW};0&g_XiaV$^lzi!FXZF#yyX4+)NkGLxW8OPe*h5rHb C?E|L( diff --git a/ziffers/__pycache__/transformer.cpython-311.pyc b/ziffers/__pycache__/transformer.cpython-311.pyc deleted file mode 100644 index dc3dc3f64432b31af5f58305bd38fbdb55d02d74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8285 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 C?f#np diff --git a/ziffers/classes.py b/ziffers/classes.py index 7d82b78..8c8175d 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -5,11 +5,15 @@ class Meta: text: str @dataclass -class Duration(Meta): +class DurationChange(Meta): dur: float @dataclass -class Octave(Meta): +class OctaveChange(Meta): + oct: int + +@dataclass +class OctaveMod(Meta): oct: int @dataclass @@ -25,7 +29,6 @@ class Pitch(Event): @dataclass class RandomPitch(Event): pc: int = None - @dataclass class Chord(Event): @@ -42,8 +45,10 @@ class Ziffers: text: str = None def __post_init__(self): self.text = self.collect_text() - def collect_text(self): + 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] @dataclass class Sequence(Meta): diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 78b3e05..1721342 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -2,7 +2,6 @@ from lark import Transformer from .classes import * from .common import flatten from .defaults import default_durs -from collections import Counter class ZiffersTransformer(Transformer): @@ -28,11 +27,14 @@ class ZiffersTransformer(Transformer): def pc(self, s): if(len(s)>1): - counter = Counter() - for d in s: - counter.update(d) - result = dict(counter) - result["text"] = result["text"][::-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] return Pitch(**result) else: val = s[0] @@ -46,7 +48,15 @@ class ZiffersTransformer(Transformer): def oct_change(self,s): octave = s[0] - return [Octave(oct=octave["oct"],text=octave["text"]),s[1]] + return [OctaveChange(oct=octave["oct"],text=octave["text"]),s[1]] + + def oct_mod(self,s): + octave = s[0] + return [OctaveMod(oct=octave["oct"],text=octave["text"]),s[1]] + + def escaped_octave(self,s): + value = s[0][1:-1] + 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]) @@ -57,14 +67,19 @@ class ZiffersTransformer(Transformer): def dur_change(self,s): duration = s[0] - return [Duration(dur=duration["dur"], text=duration["text"]),s[1]] + return [DurationChange(dur=duration["dur"], text=duration["text"]),s[1]] - def duration(self,s): + def escaped_decimal(self,s): + val = s[0] + val["text"] = "<"+val["text"]+">" + return val + + 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[::-1]} + return {"dur": sum(durations), "text":characters} - def dur(self,s): + def dotted_dur(self,s): key = s[0] val = default_durs[key] dots = len(s)-1 @@ -72,6 +87,10 @@ class ZiffersTransformer(Transformer): val = val * (2.0-(1.0/(2*dots))) return [key+"."*dots,val] + def decimal(self,s): + val = s[0] + return {"dur": float(val),"text": val.value} + def dot(self,s): return "." diff --git a/ziffers/parser.py b/ziffers/parser.py index b131aaa..4862b59 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -9,13 +9,4 @@ grammar = grammar_path / "ziffers.lark" ziffers_parser = Lark.open(grammar, rel_to=__file__, start='value', parser='lalr', transformer=ZiffersTransformer()) def parse_expression(expr): - return ziffers_parser.parse(expr) - -if __name__ == '__main__': - print(ziffers_parser.parse("[1 [2 3]]")) - #print(ziffers_parser.parse("(1 (1,3) 1..3)")) - #print(ziffers_parser.parse("_^ q _qe^3 qww_4 _123 <1 2>")) - #print(ziffers_parser.parse("q _2 _ 3 ^ 343")) - #print(ziffers_parser.parse("2 qe2 e4").values) - #print(ziffers_parser.parse("q 2 <3 343>")) - #print(ziffers_parser.parse("q (2 <3 343 (3 4)>)")) + return ziffers_parser.parse(expr) \ No newline at end of file diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index 6fcd6d5..1b6c7bd 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -1,19 +1,23 @@ ?value: root - root: (pc | dur_change | oct_change | WS | chord | cycle | randompitch | range | list | subdivision)* + root: (pc | dur_change | oct_mod | oct_change | WS | chord | cycle | randompitch | range | list | subdivision)* list: "(" root ")" - randompitch: /[\(][-?0-9][,][-?0-9][\)]/ - range: /[-?0-9]\.\.[-?0-9]/ + randompitch: /\(-?[0-9],-?[0-9]\)/ + range: /-?[0-9]\.\.-?[0-9]/ cycle: "<" root ">" pc: prefix* pitch - pitch: /[-?0-9TE]/ - prefix: octave | duration - oct_change: octave WS + pitch: /-?[0-9TE]/ + 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+ - dur_change: duration WS - duration: dur+ - dur: dchar dot* + escaped_decimal: "<" decimal ">" + dur_change: (duration_chars | decimal) WS + duration_chars: dotted_dur+ + dotted_dur: dchar dot* + decimal: /-?[0-9]+\.[0-9]+/ dchar: /[mklpdcwyhnqaefsxtgujzo]/ dot: "." subitems: (pc | WS | chord | cycle | subdivision)* From c66cc9b8ee24c799e5b29ee5ef4085bee6c1d583 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 1 Feb 2023 21:45:37 +0200 Subject: [PATCH 08/44] Added ? % --- ziffers/classes.py | 6 +++++- ziffers/mapper.py | 10 ++++++++-- ziffers/ziffers.lark | 6 ++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index 8c8175d..10acb97 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -30,6 +30,10 @@ class Pitch(Event): class RandomPitch(Event): pc: int = None +@dataclass +class RandomPercent(Meta): + percent: float = None + @dataclass class Chord(Event): pcs: list[Pitch] = None @@ -63,7 +67,7 @@ class Cyclic(Sequence): cycle: int = 0 @dataclass -class RandomPitch(Meta): +class RandomInteger(Meta): min: int max: int diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 1721342..e5500a2 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -12,9 +12,9 @@ class ZiffersTransformer(Transformer): values = flatten(items[0].values) return Sequence(values=values,text="("+"".join([val.text for val in values])+")") - def randompitch(self,s): + def random_integer(self,s): val = s[0][1:-1].split(",") - return RandomPitch(min=val[0],max=val[1],text=s[0]) + return RandomInteger(min=val[0],max=val[1],text=s[0]) def range(self,s): val = s[0].split("..") @@ -74,6 +74,12 @@ class ZiffersTransformer(Transformer): val["text"] = "<"+val["text"]+">" return val + def random_pitch(self,s): + return RandomPitch(text="?") + + def random_percent(self,s): + return RandomPercent(text="%") + def duration_chars(self,s): durations = [val[1] for val in s] characters = "".join([val[0] for val in s]) diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index 1b6c7bd..106f7fa 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -1,12 +1,14 @@ ?value: root - root: (pc | dur_change | oct_mod | oct_change | WS | chord | cycle | randompitch | range | list | subdivision)* + root: (pc | dur_change | oct_mod | oct_change | WS | chord | cycle | random_integer | random_pitch | random_percent | range | list | subdivision)* list: "(" root ")" - randompitch: /\(-?[0-9],-?[0-9]\)/ + random_integer: /\(-?[0-9]+,-?[0-9]+\)/ range: /-?[0-9]\.\.-?[0-9]/ cycle: "<" root ">" 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]>/ From 63b292a7bda35ddc6fc3e7df423848a3dbdef9b5 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Fri, 3 Feb 2023 01:02:37 +0200 Subject: [PATCH 09/44] Added list operations --- defaults.ini | 4 + ziffers/classes.py | 171 +++++++++++++++++++++++++++++++++++++------ ziffers/common.py | 16 +++- ziffers/mapper.py | 73 +++++++++++++----- ziffers/parser.py | 2 +- ziffers/ziffers.lark | 64 +++++++++++----- 6 files changed, 270 insertions(+), 60 deletions(-) create mode 100644 defaults.ini 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 From 6597ca3f176c0e462db707455937ad8bd2b3bcf9 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sat, 4 Feb 2023 17:17:14 +0200 Subject: [PATCH 10/44] Added euclid parsing support --- main.py | 2 +- ziffers/classes.py | 11 ++++++++++- ziffers/mapper.py | 22 ++++++++++++++++++++-- ziffers/ziffers.lark | 14 +++++++++++--- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index fa9eba9..eae0fc9 100644 --- a/main.py +++ b/main.py @@ -24,7 +24,7 @@ if __name__ == "__main__": '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) or (q1)<6,7<(q4 q3 q2)", + '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)", diff --git a/ziffers/classes.py b/ziffers/classes.py index 594409f..061e168 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -201,4 +201,13 @@ class Atom(Item): @dataclass class Integer(Item): ''' Class for integers ''' - value: int \ No newline at end of file + value: int + +@dataclass +class Euclid(Item): + ''' Class for euclidean cycles ''' + pulses: int + length: int + onset: list + offset: list = None + rotate: int = None \ No newline at end of file diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 276fecb..3144620 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -11,7 +11,7 @@ class ZiffersTransformer(Transformer): def random_integer(self,s): val = s[0][1:-1].split(",") - return RandomInteger(min=val[0],max=val[1],text=s[0]) + return RandomInteger(min=val[0],max=val[1],text=s[0].value) def range(self,s): val = s[0].split("..") @@ -136,6 +136,9 @@ class ZiffersTransformer(Transformer): val = s.value return Integer(text=val,value=int(val)) + def number(self,s): + return s + def lisp_operation(self,s): op = s[0] values = s[1:] @@ -149,4 +152,19 @@ class ZiffersTransformer(Transformer): return Sequence(values=s) def list_op(self,s): - return ListOperation(values=s) \ No newline at end of file + return ListOperation(values=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["rotate"] = params[2] + if len(s)>2: + init["offset"] = s[2] + text = text+s[2].text + init["text"] = text + return Euclid(**init) + + def euclid_operator(self,s): + return s.value \ No newline at end of file diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index 574308a..c5a2c2a 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -1,6 +1,6 @@ // 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)* + 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 | euclid)* // Pitch classes pc: prefix* pitch @@ -13,14 +13,22 @@ // Chords chord: pc pc+ + // Valid as integer + ?number: SIGNED_NUMBER | random_integer + // List list: prefix* "(" sequence ")" // Right recursive list operation list_op: list (operator (list | number))+ operator: /([\+\-\*\/%]|<<|>>)/ - ?number: SIGNED_NUMBER + euclid: list euclid_operator list? + ?euclid_operator: /<[0-9]+,[0-9]+(,[0-9])?>/ + + // TODO: Support randomization etc. + //euclid_operator: (">" | "<") number "," number ["," number] (">" | "<") + // Lisp like list operation lisp_operation: "(" operator WS sequence ")" @@ -51,7 +59,7 @@ // TODO: Support for parenthesis? eval: "{" operation "}" operation: atom (operator atom)+ - atom: (SIGNED_NUMBER | DECIMAL | random_integer) + atom: (number | DECIMAL) %import common.NUMBER %import common.SIGNED_NUMBER From 26d3825ffb5b436d9f501c04cabf7219230294dd Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sat, 4 Feb 2023 21:12:13 +0200 Subject: [PATCH 11/44] Added repeats --- ziffers/classes.py | 63 ++++++++++++++++++++++++++------------------ ziffers/mapper.py | 48 +++++++++++++++++++++++++-------- ziffers/ziffers.lark | 12 ++++++--- 3 files changed, 84 insertions(+), 39 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index 061e168..cff89a9 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -16,6 +16,10 @@ class Item(Meta): ''' Class for all Ziffers text based items ''' text: str +@dataclass +class Whitespace(Item): + ''' Class for whitespace ''' + @dataclass class DurationChange(Item): ''' Class for changing duration ''' @@ -73,8 +77,8 @@ class dataclass_property(property): # pylint: disable=invalid-name @dataclass class Sequence(Meta): ''' Class for sequences of items''' - values: list - text: str = None + values: list[Item] + text: str = field(init=False) _text: str = field(default=None, init=False, repr=False) @dataclass_property @@ -85,23 +89,11 @@ class Sequence(Meta): 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] + wrap_start: str = field(default=None, repr=False) + wrap_end: str = field(default=None, repr=False) 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 ''' @@ -111,8 +103,13 @@ class Sequence(Meta): setattr(obj, key, value) def collect_text(self) -> str: - return "".join([val.text for val in self.values]) - + text = "".join([val.text for val in self.values]) + if self.wrap_start != None: + text = self.wrap_start + text + if self.wrap_end != None: + text = text + self.wrap_end + return text + def pcs(self) -> list[int]: return [val.pc for val in self.values if type(val) is Pitch] @@ -125,12 +122,15 @@ class Sequence(Meta): @dataclass 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) + wrap_start: str = field(default="(", repr=False) + wrap_end: str = field(default=")", repr=False) + +@dataclass +class RepeatedListSequence(Sequence): + ''' 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): @@ -141,6 +141,9 @@ class Subdivision(Item): class Cyclic(Sequence): ''' 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? @@ -189,6 +192,8 @@ class Operation(Item): class Eval(Sequence): ''' 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) @@ -210,4 +215,12 @@ class Euclid(Item): length: int onset: list offset: list = None - rotate: int = None \ No newline at end of file + rotate: int = None + +@dataclass +class RepeatedSequence(Sequence): + ''' 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/mapper.py b/ziffers/mapper.py index 3144620..5e4370a 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -6,8 +6,11 @@ import operator class ZiffersTransformer(Transformer): + def start(self,items): + return Sequence(values=items[0]) + def sequence(self,items): - return Sequence(values=flatten(items)) + return flatten(items) def random_integer(self,s): val = s[0][1:-1].split(",") @@ -18,8 +21,8 @@ class ZiffersTransformer(Transformer): return Range(start=val[0],end=val[1],text=s[0]) def cycle(self, items): - values = items[0].values - return Cyclic(values=values, wrapper="<>") + values = items[0] + return Cyclic(values=values) def pc(self, s): if(len(s)>1): @@ -95,7 +98,7 @@ class ZiffersTransformer(Transformer): return chardur def WS(self,s): - return Item(text=s[0]) + return Whitespace(text=s[0]) def subdivision(self,items): values = flatten(items[0]) @@ -108,7 +111,7 @@ class ZiffersTransformer(Transformer): def eval(self,s): val = s[0] - return Eval(values=val,wrapper="{}") + return Eval(values=val) def operation(self,s): return s @@ -122,14 +125,28 @@ class ZiffersTransformer(Transformer): 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 + values = items[-1] + seq = ListSequence(values=values,wrap_start=prefixes["text"]+"(") seq.update_values(prefixes) return seq else: - seq = items[0] - seq.wrapper = "()" + 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+")") + else: + 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+")") + else: + seq = RepeatedListSequence(values=items[-2],repeats=Integer(text='1', value=1)) return seq def SIGNED_NUMBER(self, s): @@ -139,6 +156,9 @@ class ZiffersTransformer(Transformer): def number(self,s): return s + def cyclic_number(self,s): + return Cyclic(values=s) + def lisp_operation(self,s): op = s[0] values = s[1:] @@ -167,4 +187,10 @@ class ZiffersTransformer(Transformer): return Euclid(**init) def euclid_operator(self,s): - return s.value \ No newline at end of file + return s.value + + 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')) diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index c5a2c2a..49f5558 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -1,6 +1,6 @@ // 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 | euclid)* + ?root: sequence -> start + sequence: (pc | 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)* // Pitch classes pc: prefix* pitch @@ -14,15 +14,21 @@ chord: pc pc+ // Valid as integer - ?number: SIGNED_NUMBER | random_integer + ?number: SIGNED_NUMBER | random_integer | cyclic_number + cyclic_number: "<" number (WS number)* ">" + + // Repeats + repeat: "[:" sequence ":" [number] "]" // List list: prefix* "(" sequence ")" + repeated_list: prefix* "(:" sequence ":" [number] ")" // Right recursive list operation list_op: list (operator (list | number))+ operator: /([\+\-\*\/%]|<<|>>)/ + // Euclidean cycles euclid: list euclid_operator list? ?euclid_operator: /<[0-9]+,[0-9]+(,[0-9])?>/ From 8fcf00d77dc88650e367934bd3eff578c0424be6 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 4 Feb 2023 21:11:04 +0100 Subject: [PATCH 12/44] Add long scale list --- ziffers/scale.py | 1504 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1504 insertions(+) create mode 100644 ziffers/scale.py diff --git a/ziffers/scale.py b/ziffers/scale.py new file mode 100644 index 0000000..ff2143e --- /dev/null +++ b/ziffers/scale.py @@ -0,0 +1,1504 @@ +#!/usr/bin/env python3 + +SCALES = { + "Minoric": 444, + "Thaptic": 4341, + "Lothic": 3414, + "Phratic": 4143, + "Aerathic": 1434, + "Epathic": 4323, + "Mynic": 3234, + "Rothic": 2343, + "Eporic": 3432, + "Zyphic": 4431, + "Epogic": 4314, + "Lanic": 3144, + "Pyrric": 1443, + "Aeoloric": 4413, + "Gonic": 4134, + "Dalic": 1344, + "Dygic": 3441, + "Daric": 4332, + "Lonic": 3324, + "Phradic": 3243, + "Bolic": 2433, + "Saric": 4233, + "Zoptic": 2334, + "Aeraphic": 3342, + "Byptic": 3423, + "Aeolic": 4422, + "Koptic": 4224, + "Mixolyric": 2244, + "Lydic": 2442, + "Stathic": 4242, + "Dadic": 2424, + "Phrynic": 3333, + "Epathitonic": 32322, + "Mynitonic": 23223, + "Rocritonic": 32232, + "Pentatonic": 22323, + "Thaptitonic": 23232, + "Magitonic": 43221, + "Daditonic": 32214, + "Aeolyphritonic": 22143, + "Gycritonic": 21432, + "Pyritonic": 14322, + "Gathitonic": 42321, + "Ionitonic": 23214, + "Phrynitonic": 32142, + "Stathitonic": 21423, + "Thalitonic": 14232, + "Zolitonic": 42141, + "Epogitonic": 21414, + "Lanitonic": 14142, + "Paptitonic": 41421, + "Ionacritonic": 14214, + "Phraditonic": 41412, + "Aeoloritonic": 14124, + "Gonitonic": 41241, + "Dalitonic": 12414, + "Dygitonic": 24141, + "Aeracritonic": 41232, + "Byptitonic": 12324, + "Daritonic": 23241, + "Lonitonic": 32412, + "Ionycritonic": 24123, + "Lothitonic": 41223, + "Phratonic": 12234, + "Aerathitonic": 22341, + "Saritonic": 23412, + "Zoptitonic": 34122, + "Dolitonic": 44121, + "Poritonic": 41214, + "Aerylitonic": 12144, + "Zagitonic": 21441, + "Lagitonic": 14412, + "Molitonic": 43311, + "Staptitonic": 33114, + "Mothitonic": 31143, + "Aeritonic": 11433, + "Ragitonic": 14331, + "Ionaditonic": 43212, + "Bocritonic": 32124, + "Gythitonic": 21243, + "Pagitonic": 12432, + "Aeolythitonic": 24321, + "Zacritonic": 43131, + "Laritonic": 31314, + "Thacritonic": 13143, + "Styditonic": 31431, + "Loritonic": 14313, + "Aeolyritonic": 43113, + "Goritonic": 31134, + "Aeoloditonic": 11343, + "Doptitonic": 13431, + "Aeraphitonic": 34311, + "Zathitonic": 42411, + "Raditonic": 24114, + "Stonitonic": 41142, + "Syptitonic": 11424, + "Ionythitonic": 14241, + "Aeolanitonic": 42231, + "Danitonic": 22314, + "Ionaritonic": 23142, + "Dynitonic": 31422, + "Zyditonic": 14223, + "Aeolacritonic": 42123, + "Zythitonic": 21234, + "Dyritonic": 12342, + "Koptitonic": 23421, + "Thocritonic": 34212, + "Lycritonic": 41331, + "Daptitonic": 13314, + "Kygitonic": 33141, + "Mocritonic": 31413, + "Zynitonic": 14133, + "Epygitonic": 41322, + "Zaptitonic": 13224, + "Kagitonic": 32241, + "Zogitonic": 22413, + "Epyritonic": 24132, + "Zothitonic": 41313, + "Phrolitonic": 13134, + "Ionagitonic": 31341, + "Aeolapritonic": 13413, + "Kyritonic": 34131, + "Ionyptitonic": 41133, + "Gyritonic": 11334, + "Zalitonic": 13341, + "Stolitonic": 33411, + "Bylitonic": 34113, + "Thoditonic": 33231, + "Dogitonic": 32313, + "Phralitonic": 23133, + "Garitonic": 31332, + "Soptitonic": 13323, + "Kataritonic": 33222, + "Sylitonic": 32223, + "Thonitonic": 22233, + "Phropitonic": 22332, + "Staditonic": 23322, + "Lyditonic": 33132, + "Mythitonic": 31323, + "Sogitonic": 13233, + "Gothitonic": 32331, + "Rothitonic": 23313, + "Zylitonic": 44211, + "Zoditonic": 42114, + "Zaritonic": 21144, + "Phrythitonic": 11442, + "Rolitonic": 14421, + "Ranitonic": 44112, + "Laditonic": 41124, + "Poditonic": 11244, + "Ionothitonic": 12441, + "Kanitonic": 24411, + "Ryphitonic": 43122, + "Gylitonic": 31224, + "Aeolycritonic": 12243, + "Pynitonic": 22431, + "Zanitonic": 24312, + "Phronitonic": 42312, + "Banitonic": 23124, + "Aeronitonic": 31242, + "Golitonic": 12423, + "Dyptitonic": 24231, + "Aerynitonic": 42213, + "Palitonic": 22134, + "Stothitonic": 21342, + "Aerophitonic": 13422, + "Katagitonic": 34221, + "Ionoditonic": 42132, + "Bogitonic": 21324, + "Mogitonic": 13242, + "Docritonic": 32421, + "Epaditonic": 24213, + "Mixitonic": 33321, + "Phrothitonic": 33213, + "Katycritonic": 32133, + "Ionalitonic": 21333, + "Loptitonic": 13332, + "Thyritonic": 33312, + "Thoptitonic": 33123, + "Bycritonic": 31233, + "Pathitonic": 12333, + "Myditonic": 23331, + "Bolitonic": 42222, + "Bothitonic": 22224, + "Kataditonic": 22242, + "Koditonic": 22422, + "Tholitonic": 24222, + "Epathimic": 322122, + "Mynimic": 221223, + "Rocrimic": 212232, + "Eporimic": 122322, + "Thaptimic": 223221, + "Lothimic": 232212, + "Dyrimic": 421221, + "Koptimic": 212214, + "Thocrimic": 122142, + "Aeolanimic": 221421, + "Danimic": 214212, + "Ionarimic": 142122, + "Daptimic": 414111, + "Kygimic": 141114, + "Mocrimic": 411141, + "Zynimic": 111414, + "Aeolimic": 114141, + "Zythimic": 141411, + "Epygimic": 412311, + "Zaptimic": 123114, + "Kagimic": 231141, + "Zogimic": 311412, + "Epyrimic": 114123, + "Lycrimic": 141231, + "Bylimic": 412221, + "Zothimic": 122214, + "Phrolimic": 222141, + "Ionagimic": 221412, + "Aeolaphimic": 214122, + "Kycrimic": 141222, + "Garimic": 412212, + "Soptimic": 122124, + "Ionyptimic": 221241, + "Gyrimic": 212412, + "Zalimic": 124122, + "Stolimic": 241221, + "Thonimic": 411411, + "Stadimic": 114114, + "Thodimic": 141141, + "Mythimic": 411321, + "Sogimic": 113214, + "Gogimic": 132141, + "Rothimic": 321411, + "Katarimic": 214113, + "Sylimic": 141132, + "Mixolimic": 323211, + "Dadimic": 232113, + "Aeolyphimic": 321132, + "Gycrimic": 211323, + "Pyrimic": 113232, + "Lydimic": 132321, + "Ionacrimic": 323112, + "Gathimic": 231123, + "Ionynimic": 311232, + "Phrynimic": 112323, + "Stathimic": 123231, + "Thatimic": 232311, + "Dalimic": 322311, + "Dygimic": 223113, + "Zolimic": 231132, + "Epogimic": 311322, + "Lanimic": 113223, + "Paptimic": 132231, + "Darmic": 322212, + "Lonimic": 222123, + "Ionycrimic": 221232, + "Phradimic": 212322, + "Aeolorimic": 123222, + "Gonimic": 232221, + "Phracrimic": 321222, + "Aerathimic": 212223, + "Sarimic": 122232, + "Zoptimic": 222321, + "Zeracrimic": 223212, + "Byptimic": 232122, + "Starimic": 432111, + "Phrathimic": 321114, + "Saptimic": 211143, + "Aerodimic": 111432, + "Macrimic": 114321, + "Rogimic": 143211, + "Bygimic": 431121, + "Thycrimic": 311214, + "Aeoladimic": 112143, + "Dylimic": 121431, + "Eponimic": 214311, + "Katygimic": 143112, + "Stalimic": 423111, + "Stoptimic": 231114, + "Zygimic": 311142, + "Kataptimic": 111423, + "Aeolaptimic": 114231, + "Pothimic": 142311, + "Rycrimic": 422121, + "Ronimic": 221214, + "Stycrimic": 212142, + "Katorimic": 121422, + "Epythimic": 214221, + "Kaptimic": 142212, + "Katythimic": 421311, + "Madimic": 213114, + "Aerygimic": 131142, + "Pylimic": 311421, + "Ionathimic": 114213, + "Morimic": 142131, + "Aerycrimic": 421131, + "Ganimic": 211314, + "Eparimic": 113142, + "Lyrimic": 131421, + "Phraptimic": 314211, + "Bacrimic": 142113, + "Phralimic": 413211, + "Phrogimic": 132114, + "Rathimic": 321141, + "Katocrimic": 211413, + "Phryptimic": 114132, + "Katynimic": 141321, + "Solimic": 413121, + "Ionolimic": 131214, + "Ionophimic": 312141, + "Aeologimic": 121413, + "Zadimic": 214131, + "Sygimic": 141312, + "Thogimic": 413112, + "Rythimic": 131124, + "Donimic": 311241, + "Aeoloptimic": 112413, + "Panimic": 124131, + "Lodimic": 241311, + "Laptimic": 412131, + "Lygimic": 121314, + "Logimic": 213141, + "Lalimic": 131412, + "Sothimic": 314121, + "Phrocrimic": 141213, + "Modimic": 412122, + "Barimic": 121224, + "Poptimic": 212241, + "Sagimic": 122412, + "Aelothimic": 224121, + "Socrimic": 241212, + "Syrimic": 412113, + "Stodimic": 121134, + "Ionocrimic": 211341, + "Zycrimic": 113412, + "Ionygimic": 134121, + "Katathimic": 341211, + "Bolimic": 411312, + "Bothimic": 113124, + "Katadimic": 131241, + "Kodimic": 312411, + "Tholimic": 124113, + "Ralimic": 241131, + "Kanimic": 411231, + "Zylimic": 112314, + "Zodimic": 123141, + "Zarimic": 231411, + "Phrythimic": 314112, + "Rorimic": 141123, + "Pynimic": 411132, + "Zanimic": 111324, + "Ranimic": 113241, + "Ladimic": 132411, + "Podimic": 324111, + "Ionothimic": 241113, + "Kytrimic": 411123, + "Golimic": 111234, + "Dyptimic": 112341, + "Ryrimic": 123411, + "Gylimic": 234111, + "Aeolycrimic": 341112, + "Palimic": 332211, + "Stothimic": 322113, + "Aeronimic": 221133, + "Katagimic": 211332, + "Phronimic": 113322, + "Banimic": 133221, + "Ionodimic": 331311, + "Bogimic": 313113, + "Mogimic": 131133, + "Docrimic": 311331, + "Epadimic": 113313, + "Aerynimic": 133131, + "Mydimic": 331131, + "Thyptimic": 311313, + "Phrothimic": 113133, + "Katycrimic": 131331, + "Ionalimic": 313311, + "Loptimic": 133113, + "Zagimic": 331122, + "Lagimic": 311223, + "Thyrimic": 112233, + "Thothimic": 122331, + "Bycrimic": 223311, + "Pathimic": 233112, + "Mothimic": 322131, + "Aeranimic": 221313, + "Ragimic": 213132, + "Dolimic": 131322, + "Porimic": 313221, + "Aerylimic": 132213, + "Bocrimic": 321312, + "Gythimic": 213123, + "Pagimic": 131232, + "Aeolythimic": 312321, + "Molimic": 123213, + "Staptimic": 232131, + "Zacrimic": 321231, + "Larimic": 212313, + "Thacrimic": 123132, + "Stydimic": 231321, + "Lorimic": 313212, + "Ionadimic": 132123, + "Ionythimic": 313131, + "Aerythimic": 131313, + "Dynimic": 313122, + "Zydimic": 131223, + "Zathimic": 312231, + "Radimic": 122313, + "Stonimic": 223131, + "Syptimic": 231312, + "Ponimic": 441111, + "Kadimic": 411114, + "Gynimic": 111144, + "Thydimic": 111441, + "Polimic": 114411, + "Thanimic": 144111, + "Lathimic": 431211, + "Aeralimic": 312114, + "Kynimic": 121143, + "Stynimic": 211431, + "Epytimic": 114312, + "Katoptimic": 143121, + "Galimic": 431112, + "Kathimic": 311124, + "Lylimic": 111243, + "Epalimic": 112431, + "Epacrimic": 124311, + "Sathimic": 243111, + "Katanimic": 422211, + "Katyrimic": 222114, + "Rynimic": 221142, + "Pogimic": 211422, + "Aeraptimic": 114222, + "Epylimic": 142221, + "Manimic": 421212, + "Marimic": 212124, + "Locrimic": 121242, + "Rylimic": 212421, + "Epatimic": 124212, + "Byrimic": 242121, + "Kocrimic": 421113, + "Korimic": 211134, + "Lynimic": 111342, + "Malimic": 113421, + "Synimic": 134211, + "Phragimic": 342111, + "Mycrimic": 411222, + "Ionorimic": 112224, + "Phrydimic": 122241, + "Zyptimic": 222411, + "Katothimic": 224112, + "Phrylimic": 241122, + "Aerothimic": 411213, + "Stagimic": 112134, + "Dorimic": 121341, + "Phrycrimic": 213411, + "Kyptimic": 134112, + "Ionylimic": 341121, + "Epynimic": 333111, + "Ionogimic": 331113, + "Kydimic": 311133, + "Gaptimic": 111333, + "Tharimic": 113331, + "Ionaphimic": 133311, + "Thoptimic": 332121, + "Bagimic": 321213, + "Kyrimic": 212133, + "Sonimic": 121332, + "Aeolonimic": 213321, + "Rygimic": 133212, + "Thagimic": 332112, + "Kolimic": 321123, + "Dycrimic": 211233, + "Epycrimic": 112332, + "Gocrimic": 123321, + "Katolimic": 233211, + "Dagimic": 331221, + "Aeolydimic": 312213, + "Parimic": 122133, + "Ionaptimic": 221331, + "Thylimic": 213312, + "Lolimic": 133122, + "Thalimic": 331212, + "Stygimic": 312123, + "Aeolygimic": 121233, + "Aerogimic": 212331, + "Dacrimic": 123312, + "Baptimic": 233121, + "Stythimic": 323121, + "Kothimic": 231213, + "Pygimic": 312132, + "Rodimic": 121323, + "Sorimic": 213231, + "Monimic": 132312, + "Aeragimic": 322221, + "Epothimic": 222213, + "Salimic": 222132, + "Lyptimic": 221322, + "Katonimic": 213222, + "Gygimic": 132222, + "Aeradimic": 321321, + "Zyrimic": 213213, + "Stylimic": 132132, + "Lythimic": 312312, + "Dodimic": 123123, + "Katalimic": 231231, + "Boptimic": 312222, + "Stogimic": 122223, + "Thynimic": 222231, + "Aeolathimic": 222312, + "Bythimic": 223122, + "Padimic": 231222, + "Dathimic": 422112, + "Epagimic": 221124, + "Raptimic": 211242, + "Epolimic": 112422, + "Sythimic": 124221, + "Sydimic": 242211, + "Gacrimic": 421122, + "Borimic": 211224, + "Sycrimic": 112242, + "Gadimic": 122421, + "Aeolocrimic": 224211, + "Phrygimic": 242112, + "WholeTone": 222222, + "Lydian": 2221221, + "Mixolydian": 2212212, + "Aeolian": 2122122, + "Locrian": 1221222, + "Ionian": 2212221, + "Dorian": 2122212, + "Phrygian": 1222122, + "Ionythian": 4122111, + "Aeolyrian": 1221114, + "Gorian": 2211141, + "Aeolodian": 2111412, + "Doptian": 1114122, + "Aeraphian": 1141221, + "Zacrian": 1412211, + "Ionarian": 4113111, + "Dynian": 1131114, + "Zydian": 1311141, + "Zathian": 3111411, + "Radian": 1114113, + "Stonian": 1141131, + "Syptian": 1411311, + "Aeolacrian": 4111311, + "Zythian": 1113114, + "Dyrian": 1131141, + "Koptian": 1311411, + "Thocrian": 3114111, + "Aeolanian": 1141113, + "Danian": 1411131, + "Zogian": 4111221, + "Epyrian": 1112214, + "Lycrian": 1122141, + "Daptian": 1221411, + "Kygian": 2214111, + "Mocrian": 2141112, + "Zynian": 1411122, + "Phrolian": 3221211, + "Ionagian": 2212113, + "Aeodian": 2121132, + "Kycrian": 1211322, + "Epygian": 2113221, + "Zaptian": 1132212, + "Kagian": 1322121, + "Soptian": 3221112, + "Ionyptian": 2211123, + "Gyrian": 2111232, + "Zalian": 1112322, + "Stolian": 1123221, + "Bylian": 1232211, + "Zothian": 2322111, + "Thonian": 3212211, + "Phrorian": 2122113, + "Stadian": 1221132, + "Thodian": 2211321, + "Dogian": 2113212, + "Mixopyrian": 1132122, + "Garian": 1321221, + "Epathian": 3211311, + "Mythian": 2113113, + "Sogian": 1131132, + "Gogian": 1311321, + "Rothian": 3113211, + "Katarian": 1132113, + "Stylian": 1321131, + "Stathian": 3211122, + "Mixonyphian": 2111223, + "Magian": 1112232, + "Dadian": 1122321, + "Aeolylian": 1223211, + "Gycrian": 2232111, + "Pyrian": 2321112, + "Epogian": 3113112, + "Lanian": 1131123, + "Paptian": 1311231, + "Ionacrian": 3112311, + "Gathian": 1123113, + "Ionyphian": 1231131, + "Phrynian": 2311311, + "Ionycrian": 3112212, + "Phradian": 1122123, + "Aeolorian": 1221231, + "Gonian": 2212311, + "Dalian": 2123112, + "Dygian": 1231122, + "Zolian": 2311221, + "Aerathian": 3112122, + "Sarian": 1121223, + "Zoptian": 1212231, + "Aeracrian": 2122311, + "Byptian": 1223112, + "Darian": 2231121, + "Lonian": 2311212, + "Aeopian": 4212111, + "Rygian": 2121114, + "Epynian": 1211142, + "Ionogian": 2111421, + "Kydian": 1114212, + "Gaptian": 1142121, + "Tharian": 1421211, + "Epycrian": 4211121, + "Gocrian": 2111214, + "Katolian": 1112142, + "Thoptian": 1121421, + "Bagian": 1214211, + "Kyrian": 2142111, + "Sonian": 1421112, + "Parian": 4131111, + "Ionaptian": 1311114, + "Thylian": 3111141, + "Lolian": 1111413, + "Thagian": 1114131, + "Kolian": 1141311, + "Dycrian": 1413111, + "Stygian": 4121211, + "Aeolygian": 1212114, + "Aerogian": 2121141, + "Dacrian": 1211412, + "Baptian": 2114121, + "Dagian": 1141212, + "Aeolydian": 1412121, + "Stythian": 4121121, + "Kothian": 1211214, + "Pygian": 2112141, + "Rodian": 1121412, + "Sorian": 1214121, + "Monian": 2141211, + "Thalian": 1412112, + "Zorian": 4121112, + "Aeragian": 1211124, + "Epothian": 2111241, + "Salian": 1112412, + "Lyptian": 1124121, + "Katonian": 1241211, + "Gyphian": 2412111, + "Thacrian": 4112211, + "Dodian": 1122114, + "Aeolyptian": 1221141, + "Aeolonian": 2211411, + "Aeradian": 2114112, + "Aeolagian": 1141122, + "Zyrian": 1411221, + "Aeolathian": 4112121, + "Bythian": 1121214, + "Padian": 1212141, + "Rolian": 2121411, + "Pydian": 1214112, + "Thygian": 2141121, + "Katalian": 1411212, + "Saptian": 4111212, + "Aerodian": 1112124, + "Macrian": 1121241, + "Rogian": 1212411, + "Boptian": 2124111, + "Stogian": 1241112, + "Thynian": 2411121, + "Thycrian": 4111131, + "Aeoladian": 1111314, + "Dylian": 1113141, + "Eponian": 1131411, + "Katygian": 1314111, + "Starian": 3141111, + "Phrathian": 1411113, + "Stalian": 3311211, + "Stoptian": 3112113, + "Zygian": 1121133, + "Kataptian": 1211331, + "Aeolaptian": 2113311, + "Pothian": 1133112, + "Bygian": 1331121, + "Morian": 3231111, + "Rycrian": 2311113, + "Ronian": 3111132, + "Stycrian": 1111323, + "Katorian": 1113231, + "Epythian": 1132311, + "Kaptian": 1323111, + "Phraptian": 3222111, + "Bacrian": 2221113, + "Katythian": 2211132, + "Madian": 2111322, + "Aerygian": 1113222, + "Pylian": 1132221, + "Ionathian": 1322211, + "Katocrian": 3213111, + "Phryptian": 2131113, + "Katynian": 1311132, + "Aerycrian": 3111321, + "Ganian": 1113213, + "Eparian": 1132131, + "Lyrian": 1321311, + "Ionopian": 3212112, + "Aeologian": 2121123, + "Zadian": 1211232, + "Sygian": 2112321, + "Phralian": 1123212, + "Phrogian": 1232121, + "Rathian": 2321211, + "Rythian": 3211212, + "Donian": 2112123, + "Aeoloptian": 1121232, + "Panian": 1212321, + "Lodian": 2123211, + "Solian": 1232112, + "Ionolian": 2321121, + "Laptian": 3211131, + "Lygian": 2111313, + "Logian": 1113132, + "Lalian": 1131321, + "Sothian": 1313211, + "Phrocrian": 3132111, + "Thogian": 1321113, + "Katathian": 3131211, + "Modian": 1312113, + "Barian": 3121131, + "Mixolocrian": 1211313, + "Sagian": 2113131, + "Aeolothian": 1131312, + "Socrian": 1313121, + "Tholian": 3131121, + "Ralian": 1311213, + "Syrian": 3112131, + "Stodian": 1121313, + "Ionocrian": 1213131, + "Zycrian": 2131311, + "Ionygian": 1313112, + "Zarian": 3131112, + "Phrythian": 1311123, + "Rorian": 3111231, + "Bolian": 1112313, + "Bothian": 1123131, + "Katadian": 1231311, + "Kodian": 2313111, + "Ranian": 3123111, + "Ladian": 1231113, + "Podian": 2311131, + "Ionothian": 3111312, + "Kanian": 1113123, + "Zylian": 1131231, + "Zodian": 1312311, + "Golian": 3122211, + "Dyptian": 1222113, + "Ryphian": 2221131, + "Gylian": 2211312, + "Aeolycrian": 2113122, + "Pynian": 1131222, + "Zanian": 1312221, + "Palian": 3122121, + "Stothian": 1221213, + "Aerorian": 2212131, + "Katagian": 2121312, + "Phronian": 1213122, + "Banian": 2131221, + "Aeronian": 1312212, + "Loptian": 3121311, + "Ionodian": 1213113, + "Bogian": 2131131, + "Mogian": 1311312, + "Docrian": 3113121, + "Epadian": 1131213, + "Aerynian": 1312131, + "Bycrian": 3121221, + "Pathian": 1212213, + "Mydian": 2122131, + "Thyptian": 1221312, + "Phrothian": 2213121, + "Katycrian": 2131212, + "Ionalian": 1312122, + "Dolian": 3112221, + "Porian": 1122213, + "Aerylian": 1222131, + "Zagian": 2221311, + "Lagian": 2213112, + "Tyrian": 2131122, + "Mixonorian": 1311222, + "Pagian": 3111222, + "Aeolythian": 1112223, + "Molian": 1122231, + "Staptian": 1222311, + "Mothian": 2223111, + "Aeranian": 2231112, + "Ragian": 2311122, + "Larian": 2222121, + "Lythian": 2221212, + "Stydian": 2212122, + "Lorian": 2121222, + "Ionadian": 1212222, + "Bocrian": 2122221, + "Mixolythian": 1222212, + "Thadian": 4311111, + "Sanian": 3111114, + "Ionydian": 1111143, + "Epydian": 1111431, + "Katydian": 1114311, + "Mathian": 1143111, + "Aeryptian": 1431111, + "Pythian": 4221111, + "Katylian": 2211114, + "Bydian": 2111142, + "Bynian": 1111422, + "Galian": 1114221, + "Zonian": 1142211, + "Myrian": 1422111, + "Katogian": 4211211, + "Stacrian": 2112114, + "Styrian": 1121142, + "Ionyrian": 1211421, + "Phrodian": 2114211, + "Pycrian": 1142112, + "Gyptian": 1421121, + "Katacrian": 4112112, + "Sodian": 1121124, + "Bathian": 1211241, + "Mylian": 2112411, + "Godian": 1124112, + "Thorian": 1241121, + "Zocrian": 2411211, + "Stanian": 4111122, + "Epanian": 1111224, + "Konian": 1112241, + "Stocrian": 1122411, + "Kalian": 1224111, + "Phroptian": 2241111, + "Dydian": 2411112, + "Katyptian": 4111113, + "Epodian": 1111134, + "Mygian": 1111341, + "Pacrian": 1113411, + "Aerocrian": 1134111, + "Aeolarian": 1341111, + "Kythian": 3411111, + "Bonian": 3321111, + "Badian": 3211113, + "Katodian": 2111133, + "Sadian": 1111332, + "Dothian": 1113321, + "Moptian": 1133211, + "Aeryrian": 1332111, + "Epagian": 3312111, + "Raptian": 3121113, + "Epolian": 1211133, + "Sythian": 2111331, + "Sydian": 1113312, + "Epocrian": 1133121, + "Kylian": 1331211, + "Gacrian": 3311121, + "Borian": 3111213, + "Sycrian": 1112133, + "Gadian": 1121331, + "Aeolocrian": 1213311, + "Mixodorian": 2133111, + "Dathian": 1331112, + "Katoptian": 3311112, + "Ponian": 3111123, + "Kadian": 1111233, + "Gynian": 1112331, + "Thyphian": 1123311, + "Polian": 1233111, + "Thanian": 2331111, + "Epacrian": 3221121, + "Sathian": 2211213, + "Lathian": 2112132, + "Aeralian": 1121322, + "Kynian": 1213221, + "Stynian": 2132211, + "Epyphian": 1322112, + "Pogian": 3212121, + "Aeraptian": 2121213, + "Epylian": 1212132, + "Gamian": 2121321, + "Kathian": 1213212, + "Lylian": 2132121, + "Epalian": 1321212, + "Eporian": 3211221, + "Rylian": 2112213, + "Epaptian": 1122132, + "Byrian": 1221321, + "Katanian": 2213211, + "Katyrian": 2132112, + "Rynian": 1321122, + "Korian": 3122112, + "Lynian": 1221123, + "Malian": 2211231, + "Synian": 2112312, + "Phragian": 1123122, + "Manian": 1231221, + "Marian": 2312211, + "Mycrian": 3121212, + "Ionorian": 1212123, + "Phrydian": 2121231, + "Zyptian": 1212312, + "Katothian": 2123121, + "Phrylian": 1231212, + "Kocrian": 2312121, + "Ionanian": 3121122, + "Aerothian": 1211223, + "Stagian": 2112231, + "Lothian": 1122312, + "Phrycrian": 1223121, + "Kyptian": 2231211, + "Ionylian": 2312112, + "Gydian": 4211112, + "Kogian": 2111124, + "Rarian": 1111242, + "Aerolian": 1112421, + "Karian": 1124211, + "Myptian": 1242111, + "Rydian": 2421111, + "Aeolynian": 2222211, + "Aeroptian": 2222112, + "Phryrian": 2221122, + "Gothian": 2211222, + "Storian": 2112222, + "Pyptian": 1122222, + "Thydian": 1222221, + "Aerycryllic": 22122111, + "Gadyllic": 21221112, + "Solyllic": 12211122, + "Zylyllic": 22111221, + "Mixodyllic": 21112212, + "Soryllic": 11122122, + "Godyllic": 11221221, + "Epiphyllic": 12212211, + "Pynyllic": 41112111, + "Bocryllic": 11121114, + "Kogyllic": 11211141, + "Raryllic": 12111411, + "Zycryllic": 21114111, + "Mycryllic": 11141112, + "Laptyllic": 11411121, + "Pylyllic": 14111211, + "Pothyllic": 32111211, + "Phronyllic": 21112113, + "Stynyllic": 11121132, + "Rathyllic": 11211321, + "Aeryptyllic": 12113211, + "Zydyllic": 21132111, + "Katolyllic": 11321112, + "Rythyllic": 13211121, + "Locryllic": 31131111, + "Bylyllic": 11311113, + "Sogyllic": 13111131, + "Ionycryllic": 31111311, + "Koptyllic": 11113113, + "Epyryllic": 11131131, + "Soptyllic": 11311311, + "Aeolylyllic": 13113111, + "Aeracryllic": 31122111, + "Epygyllic": 11221113, + "Thonyllic": 12211131, + "Lanyllic": 22111311, + "Phrynyllic": 21113112, + "Lycryllic": 11131122, + "Ionyptyllic": 11311221, + "Epathyllic": 13112211, + "Dydyllic": 31121211, + "Thogyllic": 11212113, + "Rygyllic": 12121131, + "Bycryllic": 21211311, + "Zacryllic": 12113112, + "Panyllic": 21131121, + "Dyryllic": 11311212, + "Zathyllic": 13112121, + "Dagyllic": 31121112, + "Katalyllic": 11211123, + "Katoryllic": 12111231, + "Dodyllic": 21112311, + "Zogyllic": 11123112, + "Madyllic": 11231121, + "Dycryllic": 12311211, + "Aeologyllic": 23112111, + "Sydyllic": 31113111, + "Katogyllic": 11131113, + "Zygyllic": 11311131, + "Aeralyllic": 13111311, + "Bacryllic": 31112211, + "Aerygyllic": 11122113, + "Dathyllic": 11221131, + "Boptyllic": 12211311, + "Bagyllic": 22113111, + "Mathyllic": 21131112, + "Styptyllic": 11311122, + "Zolyllic": 13111221, + "Rocryllic": 22212111, + "Zyryllic": 22121112, + "Sagyllic": 21211122, + "Epinyllic": 12111222, + "Katagyllic": 21112221, + "Ragyllic": 11122212, + "Gothyllic": 11222121, + "Lythyllic": 12221211, + "Ionocryllic": 22211121, + "Gocryllic": 22111212, + "Epiryllic": 21112122, + "Aeradyllic": 11121222, + "Staptyllic": 11212221, + "Danyllic": 12122211, + "Goptyllic": 21222111, + "Epocryllic": 12221112, + "Ionoptyllic": 22121121, + "Aeoloryllic": 21211212, + "Thydyllic": 12112122, + "Gycryllic": 21121221, + "Lyryllic": 11212212, + "Mogyllic": 12122121, + "Katodyllic": 21221211, + "Moptyllic": 12212112, + "Dolyllic": 41211111, + "Moryllic": 12111114, + "Bydyllic": 21111141, + "Pocryllic": 11111412, + "Phracryllic": 11114121, + "Gyryllic": 11141211, + "Phrygyllic": 11412111, + "Dogyllic": 14121111, + "Thagyllic": 41121111, + "Thoptyllic": 11211114, + "Phraptyllic": 12111141, + "Gylyllic": 21111411, + "Phralyllic": 11114112, + "Dygyllic": 11141121, + "Ronyllic": 11411211, + "Epogyllic": 14112111, + "Aeoladyllic": 41111211, + "Kocryllic": 11112114, + "Lodyllic": 11121141, + "Bynyllic": 11211411, + "Kydyllic": 12114111, + "Bygyllic": 21141111, + "Phryptyllic": 11411112, + "Ionayllic": 14111121, + "Phroryllic": 41111121, + "Thyphyllic": 11111214, + "Poptyllic": 11112141, + "Mixonyllic": 11121411, + "Paptyllic": 11214111, + "Storyllic": 12141111, + "Phrycryllic": 21411111, + "Palyllic": 14111112, + "Phranyllic": 32211111, + "Stydyllic": 22111113, + "Zadyllic": 21111132, + "Zalyllic": 11111322, + "Zocryllic": 11113221, + "Katocryllic": 11132211, + "Aerathyllic": 11322111, + "Stoptyllic": 13221111, + "Lydyllic": 32121111, + "Radyllic": 21211113, + "Stagyllic": 12111132, + "Ionoryllic": 21111321, + "Phrodyllic": 11113212, + "Aeragyllic": 11132121, + "Banyllic": 11321211, + "Epothyllic": 13212111, + "Zoryllic": 32112111, + "Phrolyllic": 21121113, + "Kolyllic": 11211132, + "Thodyllic": 12111321, + "Socryllic": 21113211, + "Aeolyllic": 11132112, + "Zythyllic": 11321121, + "Aeoryllic": 13211211, + "Mixolydyllic": 32111112, + "Mixonyphyllic": 21111123, + "Aeolanyllic": 11111232, + "Thocryllic": 11112321, + "Kygyllic": 11123211, + "Ionagyllic": 11232111, + "Gogyllic": 12321111, + "Phradyllic": 23211111, + "Ioniptyllic": 31311111, + "Kycryllic": 13111113, + "Aeolaptyllic": 31111131, + "Rodyllic": 11111313, + "Ionathyllic": 11113131, + "Pythyllic": 11131311, + "Zonyllic": 11313111, + "Ryryllic": 13131111, + "Aeolothyllic": 31221111, + "Ionyryllic": 12211113, + "Rydyllic": 22111131, + "Gonyllic": 21111312, + "Rolyllic": 11113122, + "Katydyllic": 11131221, + "Zyptyllic": 11312211, + "Modyllic": 13122111, + "Maptyllic": 31212111, + "Aeraptyllic": 12121113, + "Katadyllic": 21211131, + "Magyllic": 12111312, + "Phrylyllic": 21113121, + "Epigyllic": 11131212, + "Molyllic": 11312121, + "Ponyllic": 13121211, + "Thyptyllic": 31211211, + "Ionogyllic": 12112113, + "Aeolaryllic": 21121131, + "Katygyllic": 11211312, + "Ganyllic": 12113121, + "Kyptyllic": 21131211, + "Salyllic": 11312112, + "Sanyllic": 13121121, + "Doptyllic": 31211121, + "Ionilyllic": 12111213, + "Manyllic": 21112131, + "Polyllic": 11121312, + "Stanyllic": 11213121, + "Mixotharyllic": 12131211, + "Eporyllic": 21312111, + "Aerynyllic": 13121112, + "Lonyllic": 31121121, + "Sathyllic": 11211213, + "Layllic": 12112131, + "Saryllic": 21121311, + "Thacryllic": 11213112, + "Aeolynyllic": 12131121, + "Thadyllic": 21311211, + "Lynyllic": 13112112, + "Aeolathyllic": 31112121, + "Aeolocryllic": 11121213, + "Phroptyllic": 11212131, + "Kodyllic": 12121311, + "Epaptyllic": 21213111, + "Ionoyllic": 12131112, + "Gyptyllic": 21311121, + "Aerythyllic": 13111212, + "Zagyllic": 31112112, + "Epacryllic": 11121123, + "Thorcryllic": 11211231, + "Loptyllic": 12112311, + "Katylyllic": 21123111, + "Malyllic": 11231112, + "Mydyllic": 12311121, + "Thycryllic": 23111211, + "Gythyllic": 31111221, + "Pyryllic": 11112213, + "Rycryllic": 11122131, + "Phrathyllic": 11221311, + "Badyllic": 12213111, + "Phrocryllic": 22131111, + "Staryllic": 21311112, + "Zothyllic": 13111122, + "Tharyllic": 31111212, + "Sylyllic": 11112123, + "Lothyllic": 11121231, + "Daryllic": 11212311, + "Monyllic": 12123111, + "Styryllic": 21231111, + "Aeolacryllic": 12311112, + "Raptyllic": 23111121, + "Kataryllic": 31111122, + "Aerocryllic": 11111223, + "Zanyllic": 11112231, + "Aeolonyllic": 11122311, + "Aeonyllic": 11223111, + "Kyryllic": 12231111, + "Sythyllic": 22311111, + "Katycryllic": 23111112, + "Stogyllic": 22121211, + "Ionidyllic": 21212112, + "Stonyllic": 12121122, + "Stalyllic": 21211221, + "Poryllic": 12112212, + "Mocryllic": 21122121, + "Aeolyryllic": 11221212, + "Baryllic": 12212121, + "Dalyllic": 22112121, + "Ionyphyllic": 21121212, + "Zaptyllic": 11212122, + "Garyllic": 12121221, + "Gathyllic": 21212211, + "Mixopyryllic": 12122112, + "Ionacryllic": 21221121, + "Stylyllic": 12211212, + "Stycryllic": 42111111, + "Ionothyllic": 21111114, + "Mythyllic": 11111142, + "Aerylyllic": 11111421, + "Bonyllic": 11114211, + "Tholyllic": 11142111, + "Katyryllic": 11421111, + "Sadyllic": 14211111, + "Stolyllic": 41111112, + "Logyllic": 11111124, + "Dacryllic": 11111241, + "Thynyllic": 11112411, + "Gydyllic": 11124111, + "Eparyllic": 11241111, + "Dynyllic": 12411111, + "Ionyllic": 24111111, + "Zaryllic": 33111111, + "Dythyllic": 31111113, + "Ionaryllic": 11111133, + "Laryllic": 11111331, + "Kataptyllic": 11113311, + "Sonyllic": 11133111, + "Pathyllic": 11331111, + "Loryllic": 13311111, + "Aeronyllic": 32111121, + "Pycryllic": 21111213, + "Mygyllic": 11112132, + "Lylyllic": 11121321, + "Daptyllic": 11213211, + "Ioninyllic": 12132111, + "Epaphyllic": 21321111, + "Lolyllic": 13211112, + "Stacryllic": 31211112, + "Doryllic": 12111123, + "Kadyllic": 21111231, + "Rynyllic": 11112312, + "Aerogyllic": 11123121, + "Rothyllic": 11231211, + "Kagyllic": 12312111, + "Stathyllic": 23121111, + "Thyryllic": 22221111, + "Gygyllic": 22211112, + "Sodyllic": 22111122, + "Goryllic": 21111222, + "Bothyllic": 11112222, + "Gynyllic": 11122221, + "Ionaptyllic": 11222211, + "Phryryllic": 12222111, + "Racryllic": 22211211, + "Epicryllic": 22112112, + "Stygyllic": 21121122, + "Syryllic": 11211222, + "Stythyllic": 12112221, + "Aerothyllic": 21122211, + "Mixoryllic": 11222112, + "Thanyllic": 12221121, + "Roryllic": 22112211, + "Epotyllic": 21122112, + "Epidyllic": 11221122, + "Kaptyllic": 12211221, + "MajorDimin.": 21212121, + "MinorDimin.": 12121212, + "Aerycrygic": 221112111, + "Gadygic": 211121112, + "Solygic": 111211122, + "Zylygic": 112111221, + "Garygic": 121112211, + "Sorygic": 211122111, + "Godygic": 111221112, + "Epithygic": 112211121, + "Ionoptygic": 122111211, + "Kalygic": 311211111, + "Ionodygic": 112111113, + "Bythygic": 121111131, + "Epygic": 211111311, + "Marygic": 111113112, + "Gaptygic": 111131121, + "Aeroptygic": 111311211, + "Mylygic": 113112111, + "Galygic": 131121111, + "Mixolydygic": 311121111, + "Ionycrygic": 111211113, + "Zoptygic": 112111131, + "Phrygygic": 121111311, + "Locrygic": 211113111, + "Gonygic": 111131112, + "Aeracrygic": 111311121, + "Aerathygic": 113111211, + "Dorygic": 131112111, + "Dycrygic": 311112111, + "Aeolygic": 111121113, + "Dydygic": 111211131, + "Tholygic": 112111311, + "Rynygic": 121113111, + "Bycrygic": 211131111, + "Zacrygic": 111311112, + "Panygic": 113111121, + "Dyrygic": 131111211, + "Loptygic": 311111211, + "Katylygic": 111112113, + "Phradygic": 111121131, + "Mixodygic": 111211311, + "Katalygic": 112113111, + "Katorygic": 121131111, + "Dogygic": 211311111, + "Zodygic": 113111112, + "Madygic": 131111121, + "Bagygic": 221211111, + "Mathygic": 212111112, + "Styptygic": 121111122, + "Zolygic": 211111221, + "Sydygic": 111112212, + "Katygic": 111122121, + "Zyphygic": 111221211, + "Aeralygic": 112212111, + "Ryptygic": 122121111, + "Apinygic": 221111121, + "Katagygic": 211111212, + "Radygic": 111112122, + "Gothygic": 111121221, + "Lythygic": 111212211, + "Bacrygic": 112122111, + "Aerygic": 121221111, + "Dathygic": 212211111, + "Boptygic": 122111112, + "Epyrygic": 212112111, + "Aeradygic": 121121112, + "Staptygic": 211211121, + "Danygic": 112111212, + "Goptygic": 121112121, + "Epocrygic": 211121211, + "Rocrygic": 111212112, + "Zyrygic": 112121121, + "Sadygic": 121211211, + "Aeolorygic": 212111211, + "Thydygic": 121112112, + "Gycrygic": 211121121, + "Lyrygic": 111211212, + "Modygic": 112112121, + "Katodygic": 121121211, + "Moptygic": 211212111, + "Ionocrygic": 112121112, + "Gocrygic": 121211121, + "Manygic": 411111111, + "Polygic": 111111114, + "Stanygic": 111111141, + "Thaptygic": 111111411, + "Eporygic": 111114111, + "Aerynygic": 111141111, + "Thyptygic": 111411111, + "Ionogygic": 114111111, + "Aeolarygic": 141111111, + "Sathygic": 321111111, + "Ladygic": 211111113, + "Sarygic": 111111132, + "Thacrygic": 111111321, + "Aeolynygic": 111113211, + "Thadygic": 111132111, + "Lynygic": 111321111, + "Doptygic": 113211111, + "Ionilygic": 132111111, + "Phrygic": 312111111, + "Aeranygic": 121111113, + "Dothygic": 211111131, + "Lydygic": 111111312, + "Stadygic": 111113121, + "Byptygic": 111131211, + "Stodygic": 111312111, + "Zynygic": 113121111, + "Lonygic": 131211111, + "Zothygic": 311111121, + "Aeolathygic": 111111213, + "Aeolocrygic": 111112131, + "Phroptygic": 111121311, + "Kodygic": 111213111, + "Eparygic": 112131111, + "Ionygic": 121311111, + "Gyptygic": 213111111, + "Aerythygic": 131111112, + "Aeolacrygic": 311111112, + "Raptygic": 111111123, + "Gythygic": 111111231, + "Pyrygic": 111112311, + "Rycrygic": 111123111, + "Phrathygic": 111231111, + "Badygic": 112311111, + "Phrocrygic": 123111111, + "Starygic": 231111111, + "Kyrygic": 222111111, + "Sythygic": 221111112, + "Katycrygic": 211111122, + "Tharygic": 111111222, + "Sylygic": 111112221, + "Lothygic": 111122211, + "Darygic": 111222111, + "Monygic": 112221111, + "Styrygic": 122211111, + "Porygic": 221121111, + "Mocrygic": 211211112, + "Aeolyrygic": 112111122, + "Barygic": 121111221, + "Katarygic": 211112211, + "Aerocrygic": 111122112, + "Zanygic": 111221121, + "Aeolonygic": 112211211, + "Aeolanygic": 122112111, + "Kaptygic": 221111211, + "Sacrygic": 211112112, + "Padygic": 111121122, + "Epilygic": 111211221, + "Kynygic": 112112211, + "Stophygic": 121122111, + "Ionidygic": 211221111, + "Stonygic": 112211112, + "Stalygic": 122111121, + "Koptygic": 212121111, + "Raphygic": 121211112, + "Zycrygic": 212111121, + "Mycrygic": 121111212, + "Laptygic": 211112121, + "Pylygic": 111121212, + "Rodygic": 111212121, + "Epolygic": 112121211, + "Epidygic": 121212111, + "Phronygic": 211211211, + "Stynygic": 112112112, + "Zydygic": 121121121, + "Aerycryllian": 2111211111, + "Gadyllian": 1112111112, + "Solyllian": 1121111121, + "Zyphyllian": 1211111211, + "Garyllian": 2111112111, + "Soryllian": 1111121112, + "Godyllian": 1111211121, + "Epityllian": 1112111211, + "Ionyllian": 1121112111, + "Aeoryllian": 1211121111, + "Katoryllian": 3111111111, + "Dodyllian": 1111111113, + "Zogyllian": 1111111131, + "Madyllian": 1111111311, + "Dycryllian": 1111113111, + "Aeogyllian": 1111131111, + "Dydyllian": 1111311111, + "Thogyllian": 1113111111, + "Rygyllian": 1131111111, + "Bathyllian": 1311111111, + "Sydyllian": 2211111111, + "Katogyllian": 2111111112, + "Mixodyllian": 1111111122, + "Aeradyllian": 1111111221, + "Ryptyllian": 1111112211, + "Loptyllian": 1111122111, + "Kataphyllian": 1111221111, + "Phradyllian": 1112211111, + "Dagyllian": 1122111111, + "Katyllian": 1221111111, + "Gothyllian": 2121111111, + "Lythyllian": 1211111112, + "Bacryllian": 2111111121, + "Aerygyllian": 1111111212, + "Dathyllian": 1111112121, + "Boptyllian": 1111121211, + "Bagyllian": 1111212111, + "Mathyllian": 1112121111, + "Styptyllian": 1121211111, + "Zolyllian": 1212111111, + "Staptyllian": 2112111111, + "Danyllian": 1121111112, + "Goptyllian": 1211111121, + "Epocryllian": 2111111211, + "Rocryllian": 1111112112, + "Zyryllian": 1111121121, + "Sagyllian": 1111211211, + "Epinyllian": 1112112111, + "Katagyllian": 1121121111, + "Ragyllian": 1211211111, + "Thydyllian": 2111121111, + "Epiryllian": 1111211112, + "Lyryllian": 1112111121, + "Mogyllian": 1121111211, + "Katodyllian": 1211112111, + "Aerycratic": 21111111111, + "Monatic": 11111111112, + "Solatic": 11111111121, + "Zylatic": 11111111211, + "Mixolatic": 11111112111, + "Soratic": 11111121111, + "Godatic": 11111211111, + "Eptatic": 11112111111, + "Ionatic": 11121111111, + "Aeolatic": 11211111111, + "Thydatic": 12111111111, + "Chromatic": 111111111111, +} + +def get_scale(name: str) -> list: + """Get a scale from the global scale list""" + scale = SCALES.get( + name.lower().capitalize(), + SCALES["Chromatic"]) + return list(map(int, str(scale))) + +if __name__ == "__main__": + print(get_scale("Godatic")) From 5a47deef617a23d5b2189a2fc6ecf44768dceb85 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 4 Feb 2023 21:24:42 +0100 Subject: [PATCH 13/44] Add method to transform PC into MIDI note --- ziffers/scale.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ziffers/scale.py b/ziffers/scale.py index ff2143e..0245fb4 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1493,6 +1493,7 @@ SCALES = { "Chromatic": 111111111111, } + def get_scale(name: str) -> list: """Get a scale from the global scale list""" scale = SCALES.get( @@ -1500,5 +1501,22 @@ def get_scale(name: str) -> list: SCALES["Chromatic"]) return list(map(int, str(scale))) + +def note_from_pc( + root: int, + pitch_class: int, + intervals: list[int] + ) -> int: + """Resolve a pitch class into a note from a scale""" + intervals = sum(intervals[0:pitch_class]) + return (root + intervals if pitch_class >= 0 else + root - intervals) + + if __name__ == "__main__": print(get_scale("Godatic")) + + # Taking the fifth from the chromatic scale + user_scale = get_scale("chromatic") + note = note_from_pc(60, -7, user_scale) + print(f"Note: {note} from {user_scale}") From 6e498c5f246e7d6ac90d9ad91f45e8debce6a579 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 4 Feb 2023 21:48:32 +0100 Subject: [PATCH 14/44] support cents --- ziffers/scale.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ziffers/scale.py b/ziffers/scale.py index 0245fb4..41ed252 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1505,9 +1505,12 @@ def get_scale(name: str) -> list: def note_from_pc( root: int, pitch_class: int, - intervals: list[int] + intervals: list[int|float], + cents: bool = False ) -> int: """Resolve a pitch class into a note from a scale""" + if cents: + intervals = list(map(lambda x: x / 100), intervals) intervals = sum(intervals[0:pitch_class]) return (root + intervals if pitch_class >= 0 else root - intervals) From 1710d2f67b91f130ac46d41123202cefa7f1f6e4 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 5 Feb 2023 14:12:22 +0200 Subject: [PATCH 15/44] Refactored duration changes Fixes cyclic duration changes --- ziffers/mapper.py | 22 ++++++++++++++++++++-- ziffers/ziffers.lark | 4 +++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 5e4370a..e4bfcfb 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -59,8 +59,26 @@ class ZiffersTransformer(Transformer): return Chord(pcs=s,text="".join([val.text for val in s])) def dur_change(self,s): - duration = s[0] - return [DurationChange(dur=duration["dur"], text=duration["text"]),s[1]] + durs = s[0] + return DurationChange(dur=durs[1], text=durs[0]) + + def char_change(self,s): + chars = "" + durs = 0.0 + for (dchar,dots) in s: + val = default_durs[dchar] + if(dots>0): + val = val * (2.0-(1.0/(2*dots))) + chars = chars + (dchar+"."*dots) + durs = durs + val + return [chars,durs] + + 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] def escaped_decimal(self,s): val = s[0] diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index 49f5558..4862661 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -52,7 +52,9 @@ // Control characters modifying future events oct_mod: octave WS oct_change: escaped_octave WS - dur_change: (duration_chars | decimal) WS + dur_change: (decimal | char_change) + char_change: dchar_not_prefix+ + dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?!\d)/ // Generative rules random_integer: /\(-?[0-9]+,-?[0-9]+\)/ From 89183471fd1da4969f64b1fc757501b39e019daf Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 5 Feb 2023 15:26:46 +0200 Subject: [PATCH 16/44] New features for resolving note Added octave & addition & note name parsing --- ziffers/scale.py | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/ziffers/scale.py b/ziffers/scale.py index 41ed252..ae94049 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +import re + SCALES = { "Minoric": 444, "Thaptic": 4341, @@ -1493,6 +1495,33 @@ SCALES = { "Chromatic": 111111111111, } +note_to_interval = { + "C": 0, + "D": 2, + "E": 4, + "F": 5, + "G": 7, + "A": 9, + "B": 11 +} + +modifiers = { + "#": 1, + "b": -1, + "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: + raise ValueError("Invalid note name: "+name) + values = items.groups() + octave = int(values[2]) if values[2] else 4 + modifier = modifiers[values[1]] if values[1] else 0 + 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""" @@ -1503,17 +1532,21 @@ def get_scale(name: str) -> list: def note_from_pc( - root: int, + root: int|str, pitch_class: int, intervals: list[int|float], - cents: bool = False + cents: bool = False, + octave: int = 0, + addition: int = 0 ) -> int: """Resolve a pitch class into a note from a scale""" + if type(root)==str: + root = note_to_midi(root) if cents: intervals = list(map(lambda x: x / 100), intervals) - intervals = sum(intervals[0:pitch_class]) - return (root + intervals if pitch_class >= 0 else - root - intervals) + interval_sum = sum(intervals[0:pitch_class]) + note = (root + interval_sum if pitch_class >= 0 else root - interval_sum) + return note + octave*sum(intervals) + addition if __name__ == "__main__": From 09553c539b8d790881d7c10cee419a042f33d864 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 5 Feb 2023 16:54:00 +0200 Subject: [PATCH 17/44] Added tests for scale Added option to give intervals as scale name --- tests/test_parser.py | 2 +- tests/test_scale.py | 24 ++++++++++++++++++++++++ ziffers/scale.py | 11 ++++++----- 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 tests/test_scale.py diff --git a/tests/test_parser.py b/tests/test_parser.py index 074a076..f06e7dd 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -43,4 +43,4 @@ def test_parsing_text(pattern: str): ], ) 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 \ No newline at end of file diff --git a/tests/test_scale.py b/tests/test_scale.py new file mode 100644 index 0000000..4571f9b --- /dev/null +++ b/tests/test_scale.py @@ -0,0 +1,24 @@ +from ziffers import scale +import pytest + +@pytest.mark.parametrize( + "name,expected", + [ + ("C4", 60), + ("A1", 33), + ("Bb3", 58), + ("C#1", 25), + ("foo", 60), + ], +) +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]), + ], +) +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 diff --git a/ziffers/scale.py b/ziffers/scale.py index ae94049..fa9bd2b 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1515,14 +1515,13 @@ 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: - raise ValueError("Invalid note name: "+name) + return 60 values = items.groups() octave = int(values[2]) if values[2] else 4 modifier = modifiers[values[1]] if values[1] else 0 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( @@ -1534,19 +1533,21 @@ def get_scale(name: str) -> list: def note_from_pc( root: int|str, pitch_class: int, - intervals: list[int|float], + intervals: str | list[int|float], cents: bool = False, octave: int = 0, - addition: int = 0 + modifier: int = 0 ) -> int: """Resolve a pitch class into a note from a scale""" if type(root)==str: root = note_to_midi(root) if cents: intervals = list(map(lambda x: x / 100), intervals) + if type(intervals) == str: + intervals = get_scale(intervals) interval_sum = sum(intervals[0:pitch_class]) note = (root + interval_sum if pitch_class >= 0 else root - interval_sum) - return note + octave*sum(intervals) + addition + return note + octave*sum(intervals) + modifier if __name__ == "__main__": From 79a336887c729a54e708c0b068698183807caf36 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 5 Feb 2023 16:13:02 +0100 Subject: [PATCH 18/44] minor fixes --- pyproject.toml | 2 +- ziffers/scale.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d241ae3..bd57430 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ dependencies = [ "lark>=1.1.5", "rich>=12.6.0", - "pytest=>7.2.1", + "pytest>=7.2.1", ] [project.urls] diff --git a/ziffers/scale.py b/ziffers/scale.py index fa9bd2b..1e26903 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1531,21 +1531,25 @@ def get_scale(name: str) -> list: def note_from_pc( - root: int|str, + root: int | str, pitch_class: int, intervals: str | list[int|float], cents: bool = False, octave: int = 0, - modifier: int = 0 - ) -> int: + modifier: int = 0) -> int: """Resolve a pitch class into a note from a scale""" - if type(root)==str: + + if isinstance(root, str): root = note_to_midi(root) + + if isinstance(intervals, str): + intervals = get_scale(intervals) + if cents: intervals = list(map(lambda x: x / 100), intervals) - if type(intervals) == str: - intervals = get_scale(intervals) + interval_sum = sum(intervals[0:pitch_class]) + note = (root + interval_sum if pitch_class >= 0 else root - interval_sum) return note + octave*sum(intervals) + modifier From 4d74186df8743d84898a7b97e89075ba812d79de Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 5 Feb 2023 16:20:51 +0100 Subject: [PATCH 19/44] minor fixes --- ziffers/scale.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/ziffers/scale.py b/ziffers/scale.py index 1e26903..7c5201e 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1533,31 +1533,20 @@ def get_scale(name: str) -> list: def note_from_pc( root: int | str, pitch_class: int, - intervals: str | list[int|float], + 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""" - if isinstance(root, str): - root = note_to_midi(root) + # Initialization + root = note_to_midi(root) if isinstance(root, str) else root + intervals = get_scale(intervals) if isinstance( + intervals, str) else intervals + intervals = list(map(lambda x: x / 100), intervals) if cents else intervals - if isinstance(intervals, str): - intervals = get_scale(intervals) - - if cents: - intervals = list(map(lambda x: x / 100), intervals) - - interval_sum = sum(intervals[0:pitch_class]) + # Computing the result + 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 - - -if __name__ == "__main__": - print(get_scale("Godatic")) - - # Taking the fifth from the chromatic scale - user_scale = get_scale("chromatic") - note = note_from_pc(60, -7, user_scale) - print(f"Note: {note} from {user_scale}") From 3d7fe73569b0971725c11c69df8a83275eba8ca2 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 5 Feb 2023 16:22:01 +0100 Subject: [PATCH 20/44] run black on codebase --- main.py | 60 ++++++------- repl.py | 4 +- tests/test_parser.py | 25 +++--- tests/test_scale.py | 31 ++++++- ziffers/classes.py | 149 +++++++++++++++++++++---------- ziffers/common.py | 15 ++-- ziffers/defaults.py | 72 +++++++-------- ziffers/mapper.py | 202 ++++++++++++++++++++++++------------------- ziffers/parser.py | 11 ++- ziffers/scale.py | 44 ++++------ 10 files changed, 362 insertions(+), 251 deletions(-) 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 From 903d1a6639dc06331ed65121a8fbb3595dee9a8d Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 5 Feb 2023 16:25:51 +0100 Subject: [PATCH 21/44] fixing some details in common.py --- ziffers/common.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ziffers/common.py b/ziffers/common.py index e2771a5..d3deead 100644 --- a/ziffers/common.py +++ b/ziffers/common.py @@ -1,19 +1,18 @@ -def flatten(arr) -> list: +def flatten(arr: list) -> list: """Flattens array""" return ( flatten(arr[0]) + (flatten(arr[1:]) if len(arr) > 1 else []) - if type(arr) is list - else [arr] + if isinstance(arr, list) else [arr] ) -def sum_dict(arr) -> dict: - """Sums array of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}""" +def sum_dict(arr: list[dict]) -> dict: + """Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}""" result = arr[0] - for hash in arr[1:]: + for element in arr[1:]: for key in hash.keys(): if key in result: - result[key] = result[key] + hash[key] + result[key] = result[key] + element[key] else: - result[key] = hash[key] + result[key] = element[key] return result From 9a4e73c842e0fdaa8759c3b56d1169b3301a981a Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 5 Feb 2023 16:27:44 +0100 Subject: [PATCH 22/44] make global variables obvious --- ziffers/defaults.py | 2 +- ziffers/mapper.py | 6 +++--- ziffers/parser.py | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ziffers/defaults.py b/ziffers/defaults.py index 5450226..fb0e24d 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -1,4 +1,4 @@ -default_durs = { +DEFAULT_DURS = { "m": 8.0, # 15360/1920 "k": 10240 / 1920, # ~5.333 "l": 4.0, # 7680/1920 diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 0f7ddb8..74e5d95 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -1,7 +1,7 @@ from lark import Transformer from .classes import * from .common import flatten, sum_dict -from .defaults import default_durs +from .defaults import DEFAULT_DURS import operator @@ -66,7 +66,7 @@ class ZiffersTransformer(Transformer): chars = "" durs = 0.0 for (dchar, dots) in s: - val = default_durs[dchar] + val = DEFAULT_DURS[dchar] if dots > 0: val = val * (2.0 - (1.0 / (2 * dots))) chars = chars + (dchar + "." * dots) @@ -98,7 +98,7 @@ class ZiffersTransformer(Transformer): def dotted_dur(self, s): key = s[0] - val = default_durs[key] + val = DEFAULT_DURS[key] dots = len(s) - 1 if dots > 0: val = val * (2.0 - (1.0 / (2 * dots))) diff --git a/ziffers/parser.py b/ziffers/parser.py index eea3060..e3dfe50 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -15,5 +15,6 @@ ziffers_parser = Lark.open( ) -def parse_expression(expr): +def parse_expression(expr: str): + """Parse an expression using the Ziffers parser""" return ziffers_parser.parse(expr) From 65da66ba26b6a797c7f9c929416c4044917306e8 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 5 Feb 2023 18:25:01 +0200 Subject: [PATCH 23/44] Fixed note resolver Fixed pitch class method and a typo --- ziffers/common.py | 2 +- ziffers/scale.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ziffers/common.py b/ziffers/common.py index d3deead..729306b 100644 --- a/ziffers/common.py +++ b/ziffers/common.py @@ -10,7 +10,7 @@ def sum_dict(arr: list[dict]) -> dict: """Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}""" result = arr[0] for element in arr[1:]: - for key in hash.keys(): + for key in element.keys(): if key in result: result[key] = result[key] + element[key] else: diff --git a/ziffers/scale.py b/ziffers/scale.py index 206a9a0..9127f79 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import re +from math import floor SCALES = { "Minoric": 444, @@ -1536,9 +1537,17 @@ def note_from_pc( root = note_to_midi(root) if isinstance(root, str) else root intervals = get_scale(intervals) if isinstance(intervals, str) else intervals intervals = list(map(lambda x: x / 100), intervals) if cents else intervals + scale_length = len(intervals) + + # Resolve pitch classes to the scale and calculate octave + if pitch_class>=scale_length or pitch_class<0: + octave += floor(pitch_class/scale_length) + pitch_class = scale_length-(abs(pitch_class)%scale_length) if pitch_class<0 else pitch_class%scale_length + if pitch_class == scale_length: + pitch_class = 0 # Computing the result - interval_sum = sum(intervals[0 : pitch_class % len(intervals)]) + interval_sum = sum(intervals[0 : pitch_class]) note = root + interval_sum if pitch_class >= 0 else root - interval_sum - return note + octave * sum(intervals) + modifier + return note + (octave * sum(intervals)) + modifier From d06999a11c17c42918fd797b9ab82503effa921a Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 5 Feb 2023 18:54:51 +0200 Subject: [PATCH 24/44] Simplifying --- ziffers/scale.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ziffers/scale.py b/ziffers/scale.py index 9127f79..418ae0b 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1547,7 +1547,6 @@ def note_from_pc( pitch_class = 0 # Computing the result - interval_sum = sum(intervals[0 : pitch_class]) - - note = root + interval_sum if pitch_class >= 0 else root - interval_sum + note = root + sum(intervals[0 : pitch_class]) + return note + (octave * sum(intervals)) + modifier From 006b69b42e93eb26cc689397ffe18b4dd2f8b185 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 5 Feb 2023 21:30:51 +0100 Subject: [PATCH 25/44] removing a star import --- ziffers/mapper.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 74e5d95..4d88fac 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -1,11 +1,42 @@ from lark import Transformer -from .classes import * -from .common import flatten, sum_dict +from .classes import ( + Whitespace, + DurationChange, + OctaveChange, + OctaveMod, + Pitch, + RandomPitch, + RandomPercent, + Chord, + Sequence, + ListSequence, + RepeatedListSequence, + Subdivision, + Cyclic, + RandomInteger, + Range, + Operator, + ListOperation, + Operation, + Eval, + Atom, + Integer, + Euclid, + RepeatedSequence, +) +from .common import ( + flatten, + sum_dict +) from .defaults import DEFAULT_DURS -import operator class ZiffersTransformer(Transformer): + + """ + Rules for transforming Ziffers expressions into tree. + """ + def start(self, items): return Sequence(values=items[0]) From 5836a43a807cac112d0a6cf4e971e2b8aebd8f7d Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Mon, 6 Feb 2023 19:53:26 +0200 Subject: [PATCH 26/44] First tests for the iterator --- tests/test_parser.py | 4 ++ ziffers/classes.py | 157 +++++++++++++++++++++++++++++++------------ ziffers/defaults.py | 5 ++ ziffers/mapper.py | 17 ++--- ziffers/parser.py | 40 ++++++++++- 5 files changed, 170 insertions(+), 53 deletions(-) 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 From 4e6a349ae54a05de14744aaf0adc2c3654d8284c Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Mon, 6 Feb 2023 19:55:35 +0200 Subject: [PATCH 27/44] Add ziffers class --- ziffers/mapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 2fa7149..4f60175 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -1,5 +1,6 @@ from lark import Transformer from .classes import ( + Ziffers, Whitespace, DurationChange, OctaveChange, From de4cb4ce3cc9e653cc4525a4645ac96d348d1311 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Mon, 6 Feb 2023 20:11:19 +0200 Subject: [PATCH 28/44] Fixes for first iterator tests --- ziffers/classes.py | 17 +++++++---------- ziffers/mapper.py | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index 01f3233..c1fee14 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -297,20 +297,17 @@ class RepeatedSequence(Sequence): wrap_end: str = field(default=":]", repr=False) @dataclass -class Ziffers(Meta): +class Ziffers(Sequence): """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 + super().__post_init__() + self.it = iter(self.values) def __next__(self): try: @@ -340,13 +337,13 @@ class Ziffers(Meta): # TODO: Handle options and generated values def pcs(self) -> list[int]: - return [val.pc for val in self.sequence.values if isinstance(val,Pitch)] + return [val.pc for val in self.values if isinstance(val,Pitch)] def durations(self) -> list[float]: - return [val.dur for val in self.sequence.values if isinstance(val,Pitch)] + return [val.dur for val in self.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)] + return [(val.pc,val.dur) for val in self.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 + return [val.octave for val in self.values if isinstance(val,Pitch)] \ No newline at end of file diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 4f60175..ab668f4 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -40,7 +40,7 @@ class ZiffersTransformer(Transformer): def start(self, items): seq = Sequence(values=items[0]) - return Ziffers(sequence=seq,options={}) + return Ziffers(values=seq,options={}) def sequence(self, items): return flatten(items) From ab1e9bde9a16032826cfb9ae96e4992f9d63e02b Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Mon, 6 Feb 2023 20:30:08 +0200 Subject: [PATCH 29/44] Fixed add type --- ziffers/classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index c1fee14..7ff1f52 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -318,7 +318,7 @@ class Ziffers(Sequence): 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.options[self.current.key] += self.current.value self.current = next(self.it) except StopIteration: # Start from the beginning From 9ae6f603259a08fee443bbb03699b47c081401c5 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Mon, 6 Feb 2023 20:39:17 +0200 Subject: [PATCH 30/44] Fixing but in addition --- ziffers/classes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index 7ff1f52..275cd00 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -318,7 +318,11 @@ class Ziffers(Sequence): 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 + if self.current.key in self.options: + self.options[self.current.key] += self.current.value + else: + self.options[self.current.key] = self.current.value + self.current = next(self.it) except StopIteration: # Start from the beginning From 1c4dfb99a06bbcf9d1b5a30ebe5c0047ff295b7a Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Tue, 7 Feb 2023 17:48:11 +0200 Subject: [PATCH 31/44] Pylinting --- .gitignore | 5 +- defaults.ini | 4 - main.py | 4 +- repl.py | 7 +- tests/__init__.py | 1 - tests/test_parser.py | 3 +- tests/test_scale.py | 1 + ziffers/classes.py | 269 ++++---- ziffers/common.py | 1 + ziffers/defaults.py | 1507 +++++++++++++++++++++++++++++++++++++++- ziffers/mapper.py | 246 ++++--- ziffers/parser.py | 98 ++- ziffers/scale.py | 1564 ++---------------------------------------- ziffers/ziffers.lark | 8 +- 14 files changed, 1935 insertions(+), 1783 deletions(-) delete mode 100644 defaults.ini diff --git a/.gitignore b/.gitignore index 1188230..6b303ba 100644 --- a/.gitignore +++ b/.gitignore @@ -160,7 +160,10 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ + +# VSCode +.vscode # Debugging file debug.py diff --git a/defaults.ini b/defaults.ini deleted file mode 100644 index 8659fdd..0000000 --- a/defaults.ini +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -Duration = 0.25 -Scale = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] # Chromatic - diff --git a/main.py b/main.py index 1274101..1ecf2a2 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ from ziffers import * from rich import print +# FIXME: TO BE REMOVED / CHANGED TO TEST? + if __name__ == "__main__": expressions = { "Pitches": "-2 -1 0 1 2", @@ -34,7 +36,7 @@ if __name__ == "__main__": } for ex in expressions: try: - print(f"Parsed: " + parse_expression(expressions[ex]).text) + print("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]}...") diff --git a/repl.py b/repl.py index 7df60f4..b977f67 100644 --- a/repl.py +++ b/repl.py @@ -1,10 +1,13 @@ -from ziffers import * +""" Simple REPL for testing Ziffers notation """ +# pylint: disable=locally-disabled, redefined-builtin, broad-except + from rich import print +from ziffers import parse_expression EXIT_CONDITION = ("exit", "quit", "") if __name__ == "__main__": - print(f"[purple]== ZIFFERS REPL ==[/purple]") + print("[purple]== ZIFFERS REPL ==[/purple]") while True: expr = input("> ") if expr not in EXIT_CONDITION: diff --git a/tests/__init__.py b/tests/__init__.py index 8b13789..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/test_parser.py b/tests/test_parser.py index 6482441..ed8126b 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,5 +1,6 @@ -from ziffers import * +""" Test cases for the parser """ import pytest +from ziffers import parse_expression def test_can_parse(): diff --git a/tests/test_scale.py b/tests/test_scale.py index 9256e9a..0458666 100644 --- a/tests/test_scale.py +++ b/tests/test_scale.py @@ -1,3 +1,4 @@ +""" Tests for the scale module """ from ziffers import scale import pytest diff --git a/ziffers/classes.py b/ziffers/classes.py index 275cd00..c72069a 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -1,9 +1,10 @@ +""" Ziffers classes for the parsed notation """ from dataclasses import dataclass, field +import itertools import operator -from typing import Any import random from .defaults import DEFAULT_OPTIONS -import itertools + @dataclass class Meta: @@ -14,26 +15,26 @@ 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: + if getattr(self, key) is None: setattr(self, key, value) + @dataclass class Item(Meta): """Class for all Ziffers text based items""" text: str - @dataclass class Whitespace(Item): """Class for whitespace""" - item_type: str = None + item_type: str = field(default=None, repr=False, init=False) @dataclass @@ -41,89 +42,90 @@ class DurationChange(Item): """Class for changing duration""" value: float - key: str = "duration" - item_type: str = "change" + key: str = field(default="duration", repr=False, init=False) + item_type: str = field(default="change", repr=False, init=False) + @dataclass class OctaveChange(Item): """Class for changing octave""" value: int - key: str = "octave" - item_type: str = "change" + key: str = field(default="octave", repr=False, init=False) + item_type: str = field(default="change", repr=False, init=False) @dataclass -class OctaveMod(Item): +class OctaveAdd(Item): """Class for modifying octave""" value: int - key: str = "octave" - item_type: str = "add" + key: str = field(default="octave", repr=False, init=False) + item_type: str = field(default="add", repr=False, init=False) + @dataclass class Event(Item): """Abstract class for events with duration""" - duration: float = None + duration: float = field(default=None) @dataclass class Pitch(Event): """Class for pitch in time""" - pc: int = None - duration: float = None - octave: int = None + pitch_class: int = field(default=None) + duration: float = field(default=None) + octave: int = field(default=None) @dataclass class RandomPitch(Event): """Class for random pitch""" - pc: int = None + pitch_class: int = field(default=None) @dataclass class RandomPercent(Item): """Class for random percent""" - percent: float = None + percent: float = field(default=None) @dataclass class Chord(Event): """Class for chords""" - pcs: list[Pitch] = None + pitch_classes: list[Pitch] = field(default=None) @dataclass class Function(Event): """Class for functions""" - run: str = None + run: str = field(default=None) + @dataclass class Sequence(Meta): """Class for sequences of items""" values: list[Item] - text: str = None + text: str = field(default=None) wrap_start: str = field(default=None, repr=False) wrap_end: str = field(default=None, repr=False) - local_index: int = 0 + local_index: int = field(default=0, init=False) 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)) + self.text = self.__collect_text() def __iter__(self): return self - + def __next__(self): - if self.local_index str: + def __collect_text(self) -> str: + """Collect text value from values""" text = "".join([val.text for val in self.values]) - if self.wrap_start != None: + if self.wrap_start is not None: text = self.wrap_start + text - if self.wrap_end != None: + if self.wrap_end is not None: text = text + self.wrap_end return text + def flatten_values(self): + """Flattens the Ziffers object tree""" + for item in self.values: + if isinstance(item, Sequence): + yield from item.flatten_values() + else: + yield item + + +@dataclass +class Ziffers(Sequence): + """Main class for holding options and the current state""" + + options: dict = field(default_factory=DEFAULT_OPTIONS) + loop_i: int = 0 + iterator: iter = field(default=None, repr=False) + current: Whitespace|DurationChange|OctaveChange|OctaveAdd = field(default=None) + + def __post_init__(self): + super().__post_init__() + self.iterator = self.flatten_values() + + def __next__(self): + self.current = next(self.iterator) + + # Skip whitespace and collect duration & octave changes + while isinstance( + self.current, (Whitespace, DurationChange, OctaveChange, OctaveAdd) + ): + if self.current.item_type == "change": # Change options + self.options[self.current.key] = self.current.value + elif self.current.item_type == "add": + if self.current.key in self.options: # Add to existing value + self.options[self.current.key] += self.current.value + else: # Create value if not existing + self.options[self.current.key] = self.current.value + + self.current = next(self.iterator) # Skip item + + self.current.update_new(self.options) + self.loop_i += 1 + return self.current + + def take(self, num: int) -> list[Pitch]: + """ Take number of pitch classes from the parsed sequence. Cycles from the beginning. + + Args: + num (int): Number of pitch classes to take from the sequence + + Returns: + list: List of pitch class items + """ + return list(itertools.islice(itertools.cycle(self), num)) + + def set_defaults(self, options: dict): + """ Sets options for the parser + + Args: + options (dict): Options as a dict + """ + self.options = DEFAULT_OPTIONS | options + + # TODO: Refactor these + def pitch_classes(self) -> list[int]: + """ Return list of pitch classes as ints """ + return [val.pitch_class for val in self.values if isinstance(val, Pitch)] + + def durations(self) -> list[float]: + """ Return list of pitch durations as floats""" + return [val.dur for val in self.values if isinstance(val, Pitch)] + + def pairs(self) -> list[tuple]: + """ Return list of pitches and durations """ + return [(val.pitch_class, val.dur) for val in self.values if isinstance(val, Pitch)] + + def octaves(self) -> list[int]: + """ Return list of octaves """ + return [val.octave for val in self.values if isinstance(val, Pitch)] + + @dataclass class ListSequence(Sequence): """Class for Ziffers list sequences""" @@ -154,11 +237,36 @@ class ListSequence(Sequence): wrap_end: str = field(default=")", repr=False) +@dataclass +class Integer(Item): + """Class for integers""" + + value: int + + +@dataclass +class RandomInteger(Item): + """Class for random integer""" + + 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): + """ Evaluate the random value for the generator """ + return random.randint(self.min, self.max) + + @dataclass class RepeatedListSequence(Sequence): """Class for Ziffers list sequences""" - repeats: Item = None + repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1")) wrap_start: str = field(default="(:", repr=False) wrap_end: str = field(default=":)", repr=False) @@ -181,38 +289,23 @@ 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 isinstance(val,Whitespace)] + self.values = [val for val in self.values if isinstance(val, Whitespace)] def value(self): + """ Get the value for the current cycle """ return self.values[self.cycle] def next_cycle(self, cycle: int): - self.cycle = self.cycle+1 - - -@dataclass -class RandomInteger(Item): - """Class for random integer""" - - 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) + """ Evaluate next cycle """ + self.cycle = self.cycle + 1 @dataclass class Range(Item): """Class for range""" - start: int = None - end: int = None + start: int = field(default=None) + end: int = field(default=None) ops = { @@ -239,6 +332,7 @@ class ListOperation(Sequence): """Class for list operations""" def run(self): + """ Run operations """ pass @@ -270,13 +364,6 @@ class Atom(Item): value: ... -@dataclass -class Integer(Item): - """Class for integers""" - - value: int - - @dataclass class Euclid(Item): """Class for euclidean cycles""" @@ -284,70 +371,14 @@ class Euclid(Item): pulses: int length: int onset: list - offset: list = None - rotate: int = None + offset: list = field(default=None) + rotate: int = field(default=None) @dataclass class RepeatedSequence(Sequence): """Class for repeats""" - repeats: Item = None + repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1")) wrap_start: str = field(default="[:", repr=False) wrap_end: str = field(default=":]", repr=False) - -@dataclass -class Ziffers(Sequence): - """Main class for holding options and the current state""" - - options: dict = field(default_factory=DEFAULT_OPTIONS) - loop_i: int = 0 - current: Item = None - it: iter = None - - def __post_init__(self): - super().__post_init__() - self.it = iter(self.values) - - 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": - if self.current.key in self.options: - self.options[self.current.key] += self.current.value - else: - 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.values if isinstance(val,Pitch)] - - def durations(self) -> list[float]: - return [val.dur for val in self.values if isinstance(val,Pitch)] - - def pairs(self) -> list[tuple]: - return [(val.pc,val.dur) for val in self.values if isinstance(val,Pitch)] - - def octaves(self) -> list[int]: - return [val.octave for val in self.values if isinstance(val,Pitch)] \ No newline at end of file diff --git a/ziffers/common.py b/ziffers/common.py index 729306b..11d3248 100644 --- a/ziffers/common.py +++ b/ziffers/common.py @@ -1,3 +1,4 @@ +""" Common methods used in parsing """ def flatten(arr: list) -> list: """Flattens array""" return ( diff --git a/ziffers/defaults.py b/ziffers/defaults.py index 9545711..c0727e5 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -1,3 +1,5 @@ +""" Default options for Ziffers """ + DEFAULT_DURS = { "m": 8.0, # 15360/1920 "k": 10240 / 1920, # ~5.333 @@ -39,4 +41,1507 @@ DEFAULT_DURS = { DEFAULT_OPTIONS = { "octave": 0, "duration": 0.25 -} \ No newline at end of file +} + +NOTE_TO_INTERVAL = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} + +MODIFIERS = { + "#": 1, + "b": -1, + "s": 1, +} + +# pylint: disable=locally-disabled, too-many-lines + +SCALES = { + "Minoric": 444, + "Thaptic": 4341, + "Lothic": 3414, + "Phratic": 4143, + "Aerathic": 1434, + "Epathic": 4323, + "Mynic": 3234, + "Rothic": 2343, + "Eporic": 3432, + "Zyphic": 4431, + "Epogic": 4314, + "Lanic": 3144, + "Pyrric": 1443, + "Aeoloric": 4413, + "Gonic": 4134, + "Dalic": 1344, + "Dygic": 3441, + "Daric": 4332, + "Lonic": 3324, + "Phradic": 3243, + "Bolic": 2433, + "Saric": 4233, + "Zoptic": 2334, + "Aeraphic": 3342, + "Byptic": 3423, + "Aeolic": 4422, + "Koptic": 4224, + "Mixolyric": 2244, + "Lydic": 2442, + "Stathic": 4242, + "Dadic": 2424, + "Phrynic": 3333, + "Epathitonic": 32322, + "Mynitonic": 23223, + "Rocritonic": 32232, + "Pentatonic": 22323, + "Thaptitonic": 23232, + "Magitonic": 43221, + "Daditonic": 32214, + "Aeolyphritonic": 22143, + "Gycritonic": 21432, + "Pyritonic": 14322, + "Gathitonic": 42321, + "Ionitonic": 23214, + "Phrynitonic": 32142, + "Stathitonic": 21423, + "Thalitonic": 14232, + "Zolitonic": 42141, + "Epogitonic": 21414, + "Lanitonic": 14142, + "Paptitonic": 41421, + "Ionacritonic": 14214, + "Phraditonic": 41412, + "Aeoloritonic": 14124, + "Gonitonic": 41241, + "Dalitonic": 12414, + "Dygitonic": 24141, + "Aeracritonic": 41232, + "Byptitonic": 12324, + "Daritonic": 23241, + "Lonitonic": 32412, + "Ionycritonic": 24123, + "Lothitonic": 41223, + "Phratonic": 12234, + "Aerathitonic": 22341, + "Saritonic": 23412, + "Zoptitonic": 34122, + "Dolitonic": 44121, + "Poritonic": 41214, + "Aerylitonic": 12144, + "Zagitonic": 21441, + "Lagitonic": 14412, + "Molitonic": 43311, + "Staptitonic": 33114, + "Mothitonic": 31143, + "Aeritonic": 11433, + "Ragitonic": 14331, + "Ionaditonic": 43212, + "Bocritonic": 32124, + "Gythitonic": 21243, + "Pagitonic": 12432, + "Aeolythitonic": 24321, + "Zacritonic": 43131, + "Laritonic": 31314, + "Thacritonic": 13143, + "Styditonic": 31431, + "Loritonic": 14313, + "Aeolyritonic": 43113, + "Goritonic": 31134, + "Aeoloditonic": 11343, + "Doptitonic": 13431, + "Aeraphitonic": 34311, + "Zathitonic": 42411, + "Raditonic": 24114, + "Stonitonic": 41142, + "Syptitonic": 11424, + "Ionythitonic": 14241, + "Aeolanitonic": 42231, + "Danitonic": 22314, + "Ionaritonic": 23142, + "Dynitonic": 31422, + "Zyditonic": 14223, + "Aeolacritonic": 42123, + "Zythitonic": 21234, + "Dyritonic": 12342, + "Koptitonic": 23421, + "Thocritonic": 34212, + "Lycritonic": 41331, + "Daptitonic": 13314, + "Kygitonic": 33141, + "Mocritonic": 31413, + "Zynitonic": 14133, + "Epygitonic": 41322, + "Zaptitonic": 13224, + "Kagitonic": 32241, + "Zogitonic": 22413, + "Epyritonic": 24132, + "Zothitonic": 41313, + "Phrolitonic": 13134, + "Ionagitonic": 31341, + "Aeolapritonic": 13413, + "Kyritonic": 34131, + "Ionyptitonic": 41133, + "Gyritonic": 11334, + "Zalitonic": 13341, + "Stolitonic": 33411, + "Bylitonic": 34113, + "Thoditonic": 33231, + "Dogitonic": 32313, + "Phralitonic": 23133, + "Garitonic": 31332, + "Soptitonic": 13323, + "Kataritonic": 33222, + "Sylitonic": 32223, + "Thonitonic": 22233, + "Phropitonic": 22332, + "Staditonic": 23322, + "Lyditonic": 33132, + "Mythitonic": 31323, + "Sogitonic": 13233, + "Gothitonic": 32331, + "Rothitonic": 23313, + "Zylitonic": 44211, + "Zoditonic": 42114, + "Zaritonic": 21144, + "Phrythitonic": 11442, + "Rolitonic": 14421, + "Ranitonic": 44112, + "Laditonic": 41124, + "Poditonic": 11244, + "Ionothitonic": 12441, + "Kanitonic": 24411, + "Ryphitonic": 43122, + "Gylitonic": 31224, + "Aeolycritonic": 12243, + "Pynitonic": 22431, + "Zanitonic": 24312, + "Phronitonic": 42312, + "Banitonic": 23124, + "Aeronitonic": 31242, + "Golitonic": 12423, + "Dyptitonic": 24231, + "Aerynitonic": 42213, + "Palitonic": 22134, + "Stothitonic": 21342, + "Aerophitonic": 13422, + "Katagitonic": 34221, + "Ionoditonic": 42132, + "Bogitonic": 21324, + "Mogitonic": 13242, + "Docritonic": 32421, + "Epaditonic": 24213, + "Mixitonic": 33321, + "Phrothitonic": 33213, + "Katycritonic": 32133, + "Ionalitonic": 21333, + "Loptitonic": 13332, + "Thyritonic": 33312, + "Thoptitonic": 33123, + "Bycritonic": 31233, + "Pathitonic": 12333, + "Myditonic": 23331, + "Bolitonic": 42222, + "Bothitonic": 22224, + "Kataditonic": 22242, + "Koditonic": 22422, + "Tholitonic": 24222, + "Epathimic": 322122, + "Mynimic": 221223, + "Rocrimic": 212232, + "Eporimic": 122322, + "Thaptimic": 223221, + "Lothimic": 232212, + "Dyrimic": 421221, + "Koptimic": 212214, + "Thocrimic": 122142, + "Aeolanimic": 221421, + "Danimic": 214212, + "Ionarimic": 142122, + "Daptimic": 414111, + "Kygimic": 141114, + "Mocrimic": 411141, + "Zynimic": 111414, + "Aeolimic": 114141, + "Zythimic": 141411, + "Epygimic": 412311, + "Zaptimic": 123114, + "Kagimic": 231141, + "Zogimic": 311412, + "Epyrimic": 114123, + "Lycrimic": 141231, + "Bylimic": 412221, + "Zothimic": 122214, + "Phrolimic": 222141, + "Ionagimic": 221412, + "Aeolaphimic": 214122, + "Kycrimic": 141222, + "Garimic": 412212, + "Soptimic": 122124, + "Ionyptimic": 221241, + "Gyrimic": 212412, + "Zalimic": 124122, + "Stolimic": 241221, + "Thonimic": 411411, + "Stadimic": 114114, + "Thodimic": 141141, + "Mythimic": 411321, + "Sogimic": 113214, + "Gogimic": 132141, + "Rothimic": 321411, + "Katarimic": 214113, + "Sylimic": 141132, + "Mixolimic": 323211, + "Dadimic": 232113, + "Aeolyphimic": 321132, + "Gycrimic": 211323, + "Pyrimic": 113232, + "Lydimic": 132321, + "Ionacrimic": 323112, + "Gathimic": 231123, + "Ionynimic": 311232, + "Phrynimic": 112323, + "Stathimic": 123231, + "Thatimic": 232311, + "Dalimic": 322311, + "Dygimic": 223113, + "Zolimic": 231132, + "Epogimic": 311322, + "Lanimic": 113223, + "Paptimic": 132231, + "Darmic": 322212, + "Lonimic": 222123, + "Ionycrimic": 221232, + "Phradimic": 212322, + "Aeolorimic": 123222, + "Gonimic": 232221, + "Phracrimic": 321222, + "Aerathimic": 212223, + "Sarimic": 122232, + "Zoptimic": 222321, + "Zeracrimic": 223212, + "Byptimic": 232122, + "Starimic": 432111, + "Phrathimic": 321114, + "Saptimic": 211143, + "Aerodimic": 111432, + "Macrimic": 114321, + "Rogimic": 143211, + "Bygimic": 431121, + "Thycrimic": 311214, + "Aeoladimic": 112143, + "Dylimic": 121431, + "Eponimic": 214311, + "Katygimic": 143112, + "Stalimic": 423111, + "Stoptimic": 231114, + "Zygimic": 311142, + "Kataptimic": 111423, + "Aeolaptimic": 114231, + "Pothimic": 142311, + "Rycrimic": 422121, + "Ronimic": 221214, + "Stycrimic": 212142, + "Katorimic": 121422, + "Epythimic": 214221, + "Kaptimic": 142212, + "Katythimic": 421311, + "Madimic": 213114, + "Aerygimic": 131142, + "Pylimic": 311421, + "Ionathimic": 114213, + "Morimic": 142131, + "Aerycrimic": 421131, + "Ganimic": 211314, + "Eparimic": 113142, + "Lyrimic": 131421, + "Phraptimic": 314211, + "Bacrimic": 142113, + "Phralimic": 413211, + "Phrogimic": 132114, + "Rathimic": 321141, + "Katocrimic": 211413, + "Phryptimic": 114132, + "Katynimic": 141321, + "Solimic": 413121, + "Ionolimic": 131214, + "Ionophimic": 312141, + "Aeologimic": 121413, + "Zadimic": 214131, + "Sygimic": 141312, + "Thogimic": 413112, + "Rythimic": 131124, + "Donimic": 311241, + "Aeoloptimic": 112413, + "Panimic": 124131, + "Lodimic": 241311, + "Laptimic": 412131, + "Lygimic": 121314, + "Logimic": 213141, + "Lalimic": 131412, + "Sothimic": 314121, + "Phrocrimic": 141213, + "Modimic": 412122, + "Barimic": 121224, + "Poptimic": 212241, + "Sagimic": 122412, + "Aelothimic": 224121, + "Socrimic": 241212, + "Syrimic": 412113, + "Stodimic": 121134, + "Ionocrimic": 211341, + "Zycrimic": 113412, + "Ionygimic": 134121, + "Katathimic": 341211, + "Bolimic": 411312, + "Bothimic": 113124, + "Katadimic": 131241, + "Kodimic": 312411, + "Tholimic": 124113, + "Ralimic": 241131, + "Kanimic": 411231, + "Zylimic": 112314, + "Zodimic": 123141, + "Zarimic": 231411, + "Phrythimic": 314112, + "Rorimic": 141123, + "Pynimic": 411132, + "Zanimic": 111324, + "Ranimic": 113241, + "Ladimic": 132411, + "Podimic": 324111, + "Ionothimic": 241113, + "Kytrimic": 411123, + "Golimic": 111234, + "Dyptimic": 112341, + "Ryrimic": 123411, + "Gylimic": 234111, + "Aeolycrimic": 341112, + "Palimic": 332211, + "Stothimic": 322113, + "Aeronimic": 221133, + "Katagimic": 211332, + "Phronimic": 113322, + "Banimic": 133221, + "Ionodimic": 331311, + "Bogimic": 313113, + "Mogimic": 131133, + "Docrimic": 311331, + "Epadimic": 113313, + "Aerynimic": 133131, + "Mydimic": 331131, + "Thyptimic": 311313, + "Phrothimic": 113133, + "Katycrimic": 131331, + "Ionalimic": 313311, + "Loptimic": 133113, + "Zagimic": 331122, + "Lagimic": 311223, + "Thyrimic": 112233, + "Thothimic": 122331, + "Bycrimic": 223311, + "Pathimic": 233112, + "Mothimic": 322131, + "Aeranimic": 221313, + "Ragimic": 213132, + "Dolimic": 131322, + "Porimic": 313221, + "Aerylimic": 132213, + "Bocrimic": 321312, + "Gythimic": 213123, + "Pagimic": 131232, + "Aeolythimic": 312321, + "Molimic": 123213, + "Staptimic": 232131, + "Zacrimic": 321231, + "Larimic": 212313, + "Thacrimic": 123132, + "Stydimic": 231321, + "Lorimic": 313212, + "Ionadimic": 132123, + "Ionythimic": 313131, + "Aerythimic": 131313, + "Dynimic": 313122, + "Zydimic": 131223, + "Zathimic": 312231, + "Radimic": 122313, + "Stonimic": 223131, + "Syptimic": 231312, + "Ponimic": 441111, + "Kadimic": 411114, + "Gynimic": 111144, + "Thydimic": 111441, + "Polimic": 114411, + "Thanimic": 144111, + "Lathimic": 431211, + "Aeralimic": 312114, + "Kynimic": 121143, + "Stynimic": 211431, + "Epytimic": 114312, + "Katoptimic": 143121, + "Galimic": 431112, + "Kathimic": 311124, + "Lylimic": 111243, + "Epalimic": 112431, + "Epacrimic": 124311, + "Sathimic": 243111, + "Katanimic": 422211, + "Katyrimic": 222114, + "Rynimic": 221142, + "Pogimic": 211422, + "Aeraptimic": 114222, + "Epylimic": 142221, + "Manimic": 421212, + "Marimic": 212124, + "Locrimic": 121242, + "Rylimic": 212421, + "Epatimic": 124212, + "Byrimic": 242121, + "Kocrimic": 421113, + "Korimic": 211134, + "Lynimic": 111342, + "Malimic": 113421, + "Synimic": 134211, + "Phragimic": 342111, + "Mycrimic": 411222, + "Ionorimic": 112224, + "Phrydimic": 122241, + "Zyptimic": 222411, + "Katothimic": 224112, + "Phrylimic": 241122, + "Aerothimic": 411213, + "Stagimic": 112134, + "Dorimic": 121341, + "Phrycrimic": 213411, + "Kyptimic": 134112, + "Ionylimic": 341121, + "Epynimic": 333111, + "Ionogimic": 331113, + "Kydimic": 311133, + "Gaptimic": 111333, + "Tharimic": 113331, + "Ionaphimic": 133311, + "Thoptimic": 332121, + "Bagimic": 321213, + "Kyrimic": 212133, + "Sonimic": 121332, + "Aeolonimic": 213321, + "Rygimic": 133212, + "Thagimic": 332112, + "Kolimic": 321123, + "Dycrimic": 211233, + "Epycrimic": 112332, + "Gocrimic": 123321, + "Katolimic": 233211, + "Dagimic": 331221, + "Aeolydimic": 312213, + "Parimic": 122133, + "Ionaptimic": 221331, + "Thylimic": 213312, + "Lolimic": 133122, + "Thalimic": 331212, + "Stygimic": 312123, + "Aeolygimic": 121233, + "Aerogimic": 212331, + "Dacrimic": 123312, + "Baptimic": 233121, + "Stythimic": 323121, + "Kothimic": 231213, + "Pygimic": 312132, + "Rodimic": 121323, + "Sorimic": 213231, + "Monimic": 132312, + "Aeragimic": 322221, + "Epothimic": 222213, + "Salimic": 222132, + "Lyptimic": 221322, + "Katonimic": 213222, + "Gygimic": 132222, + "Aeradimic": 321321, + "Zyrimic": 213213, + "Stylimic": 132132, + "Lythimic": 312312, + "Dodimic": 123123, + "Katalimic": 231231, + "Boptimic": 312222, + "Stogimic": 122223, + "Thynimic": 222231, + "Aeolathimic": 222312, + "Bythimic": 223122, + "Padimic": 231222, + "Dathimic": 422112, + "Epagimic": 221124, + "Raptimic": 211242, + "Epolimic": 112422, + "Sythimic": 124221, + "Sydimic": 242211, + "Gacrimic": 421122, + "Borimic": 211224, + "Sycrimic": 112242, + "Gadimic": 122421, + "Aeolocrimic": 224211, + "Phrygimic": 242112, + "WholeTone": 222222, + "Lydian": 2221221, + "Mixolydian": 2212212, + "Aeolian": 2122122, + "Locrian": 1221222, + "Ionian": 2212221, + "Dorian": 2122212, + "Phrygian": 1222122, + "Ionythian": 4122111, + "Aeolyrian": 1221114, + "Gorian": 2211141, + "Aeolodian": 2111412, + "Doptian": 1114122, + "Aeraphian": 1141221, + "Zacrian": 1412211, + "Ionarian": 4113111, + "Dynian": 1131114, + "Zydian": 1311141, + "Zathian": 3111411, + "Radian": 1114113, + "Stonian": 1141131, + "Syptian": 1411311, + "Aeolacrian": 4111311, + "Zythian": 1113114, + "Dyrian": 1131141, + "Koptian": 1311411, + "Thocrian": 3114111, + "Aeolanian": 1141113, + "Danian": 1411131, + "Zogian": 4111221, + "Epyrian": 1112214, + "Lycrian": 1122141, + "Daptian": 1221411, + "Kygian": 2214111, + "Mocrian": 2141112, + "Zynian": 1411122, + "Phrolian": 3221211, + "Ionagian": 2212113, + "Aeodian": 2121132, + "Kycrian": 1211322, + "Epygian": 2113221, + "Zaptian": 1132212, + "Kagian": 1322121, + "Soptian": 3221112, + "Ionyptian": 2211123, + "Gyrian": 2111232, + "Zalian": 1112322, + "Stolian": 1123221, + "Bylian": 1232211, + "Zothian": 2322111, + "Thonian": 3212211, + "Phrorian": 2122113, + "Stadian": 1221132, + "Thodian": 2211321, + "Dogian": 2113212, + "Mixopyrian": 1132122, + "Garian": 1321221, + "Epathian": 3211311, + "Mythian": 2113113, + "Sogian": 1131132, + "Gogian": 1311321, + "Rothian": 3113211, + "Katarian": 1132113, + "Stylian": 1321131, + "Stathian": 3211122, + "Mixonyphian": 2111223, + "Magian": 1112232, + "Dadian": 1122321, + "Aeolylian": 1223211, + "Gycrian": 2232111, + "Pyrian": 2321112, + "Epogian": 3113112, + "Lanian": 1131123, + "Paptian": 1311231, + "Ionacrian": 3112311, + "Gathian": 1123113, + "Ionyphian": 1231131, + "Phrynian": 2311311, + "Ionycrian": 3112212, + "Phradian": 1122123, + "Aeolorian": 1221231, + "Gonian": 2212311, + "Dalian": 2123112, + "Dygian": 1231122, + "Zolian": 2311221, + "Aerathian": 3112122, + "Sarian": 1121223, + "Zoptian": 1212231, + "Aeracrian": 2122311, + "Byptian": 1223112, + "Darian": 2231121, + "Lonian": 2311212, + "Aeopian": 4212111, + "Rygian": 2121114, + "Epynian": 1211142, + "Ionogian": 2111421, + "Kydian": 1114212, + "Gaptian": 1142121, + "Tharian": 1421211, + "Epycrian": 4211121, + "Gocrian": 2111214, + "Katolian": 1112142, + "Thoptian": 1121421, + "Bagian": 1214211, + "Kyrian": 2142111, + "Sonian": 1421112, + "Parian": 4131111, + "Ionaptian": 1311114, + "Thylian": 3111141, + "Lolian": 1111413, + "Thagian": 1114131, + "Kolian": 1141311, + "Dycrian": 1413111, + "Stygian": 4121211, + "Aeolygian": 1212114, + "Aerogian": 2121141, + "Dacrian": 1211412, + "Baptian": 2114121, + "Dagian": 1141212, + "Aeolydian": 1412121, + "Stythian": 4121121, + "Kothian": 1211214, + "Pygian": 2112141, + "Rodian": 1121412, + "Sorian": 1214121, + "Monian": 2141211, + "Thalian": 1412112, + "Zorian": 4121112, + "Aeragian": 1211124, + "Epothian": 2111241, + "Salian": 1112412, + "Lyptian": 1124121, + "Katonian": 1241211, + "Gyphian": 2412111, + "Thacrian": 4112211, + "Dodian": 1122114, + "Aeolyptian": 1221141, + "Aeolonian": 2211411, + "Aeradian": 2114112, + "Aeolagian": 1141122, + "Zyrian": 1411221, + "Aeolathian": 4112121, + "Bythian": 1121214, + "Padian": 1212141, + "Rolian": 2121411, + "Pydian": 1214112, + "Thygian": 2141121, + "Katalian": 1411212, + "Saptian": 4111212, + "Aerodian": 1112124, + "Macrian": 1121241, + "Rogian": 1212411, + "Boptian": 2124111, + "Stogian": 1241112, + "Thynian": 2411121, + "Thycrian": 4111131, + "Aeoladian": 1111314, + "Dylian": 1113141, + "Eponian": 1131411, + "Katygian": 1314111, + "Starian": 3141111, + "Phrathian": 1411113, + "Stalian": 3311211, + "Stoptian": 3112113, + "Zygian": 1121133, + "Kataptian": 1211331, + "Aeolaptian": 2113311, + "Pothian": 1133112, + "Bygian": 1331121, + "Morian": 3231111, + "Rycrian": 2311113, + "Ronian": 3111132, + "Stycrian": 1111323, + "Katorian": 1113231, + "Epythian": 1132311, + "Kaptian": 1323111, + "Phraptian": 3222111, + "Bacrian": 2221113, + "Katythian": 2211132, + "Madian": 2111322, + "Aerygian": 1113222, + "Pylian": 1132221, + "Ionathian": 1322211, + "Katocrian": 3213111, + "Phryptian": 2131113, + "Katynian": 1311132, + "Aerycrian": 3111321, + "Ganian": 1113213, + "Eparian": 1132131, + "Lyrian": 1321311, + "Ionopian": 3212112, + "Aeologian": 2121123, + "Zadian": 1211232, + "Sygian": 2112321, + "Phralian": 1123212, + "Phrogian": 1232121, + "Rathian": 2321211, + "Rythian": 3211212, + "Donian": 2112123, + "Aeoloptian": 1121232, + "Panian": 1212321, + "Lodian": 2123211, + "Solian": 1232112, + "Ionolian": 2321121, + "Laptian": 3211131, + "Lygian": 2111313, + "Logian": 1113132, + "Lalian": 1131321, + "Sothian": 1313211, + "Phrocrian": 3132111, + "Thogian": 1321113, + "Katathian": 3131211, + "Modian": 1312113, + "Barian": 3121131, + "Mixolocrian": 1211313, + "Sagian": 2113131, + "Aeolothian": 1131312, + "Socrian": 1313121, + "Tholian": 3131121, + "Ralian": 1311213, + "Syrian": 3112131, + "Stodian": 1121313, + "Ionocrian": 1213131, + "Zycrian": 2131311, + "Ionygian": 1313112, + "Zarian": 3131112, + "Phrythian": 1311123, + "Rorian": 3111231, + "Bolian": 1112313, + "Bothian": 1123131, + "Katadian": 1231311, + "Kodian": 2313111, + "Ranian": 3123111, + "Ladian": 1231113, + "Podian": 2311131, + "Ionothian": 3111312, + "Kanian": 1113123, + "Zylian": 1131231, + "Zodian": 1312311, + "Golian": 3122211, + "Dyptian": 1222113, + "Ryphian": 2221131, + "Gylian": 2211312, + "Aeolycrian": 2113122, + "Pynian": 1131222, + "Zanian": 1312221, + "Palian": 3122121, + "Stothian": 1221213, + "Aerorian": 2212131, + "Katagian": 2121312, + "Phronian": 1213122, + "Banian": 2131221, + "Aeronian": 1312212, + "Loptian": 3121311, + "Ionodian": 1213113, + "Bogian": 2131131, + "Mogian": 1311312, + "Docrian": 3113121, + "Epadian": 1131213, + "Aerynian": 1312131, + "Bycrian": 3121221, + "Pathian": 1212213, + "Mydian": 2122131, + "Thyptian": 1221312, + "Phrothian": 2213121, + "Katycrian": 2131212, + "Ionalian": 1312122, + "Dolian": 3112221, + "Porian": 1122213, + "Aerylian": 1222131, + "Zagian": 2221311, + "Lagian": 2213112, + "Tyrian": 2131122, + "Mixonorian": 1311222, + "Pagian": 3111222, + "Aeolythian": 1112223, + "Molian": 1122231, + "Staptian": 1222311, + "Mothian": 2223111, + "Aeranian": 2231112, + "Ragian": 2311122, + "Larian": 2222121, + "Lythian": 2221212, + "Stydian": 2212122, + "Lorian": 2121222, + "Ionadian": 1212222, + "Bocrian": 2122221, + "Mixolythian": 1222212, + "Thadian": 4311111, + "Sanian": 3111114, + "Ionydian": 1111143, + "Epydian": 1111431, + "Katydian": 1114311, + "Mathian": 1143111, + "Aeryptian": 1431111, + "Pythian": 4221111, + "Katylian": 2211114, + "Bydian": 2111142, + "Bynian": 1111422, + "Galian": 1114221, + "Zonian": 1142211, + "Myrian": 1422111, + "Katogian": 4211211, + "Stacrian": 2112114, + "Styrian": 1121142, + "Ionyrian": 1211421, + "Phrodian": 2114211, + "Pycrian": 1142112, + "Gyptian": 1421121, + "Katacrian": 4112112, + "Sodian": 1121124, + "Bathian": 1211241, + "Mylian": 2112411, + "Godian": 1124112, + "Thorian": 1241121, + "Zocrian": 2411211, + "Stanian": 4111122, + "Epanian": 1111224, + "Konian": 1112241, + "Stocrian": 1122411, + "Kalian": 1224111, + "Phroptian": 2241111, + "Dydian": 2411112, + "Katyptian": 4111113, + "Epodian": 1111134, + "Mygian": 1111341, + "Pacrian": 1113411, + "Aerocrian": 1134111, + "Aeolarian": 1341111, + "Kythian": 3411111, + "Bonian": 3321111, + "Badian": 3211113, + "Katodian": 2111133, + "Sadian": 1111332, + "Dothian": 1113321, + "Moptian": 1133211, + "Aeryrian": 1332111, + "Epagian": 3312111, + "Raptian": 3121113, + "Epolian": 1211133, + "Sythian": 2111331, + "Sydian": 1113312, + "Epocrian": 1133121, + "Kylian": 1331211, + "Gacrian": 3311121, + "Borian": 3111213, + "Sycrian": 1112133, + "Gadian": 1121331, + "Aeolocrian": 1213311, + "Mixodorian": 2133111, + "Dathian": 1331112, + "Katoptian": 3311112, + "Ponian": 3111123, + "Kadian": 1111233, + "Gynian": 1112331, + "Thyphian": 1123311, + "Polian": 1233111, + "Thanian": 2331111, + "Epacrian": 3221121, + "Sathian": 2211213, + "Lathian": 2112132, + "Aeralian": 1121322, + "Kynian": 1213221, + "Stynian": 2132211, + "Epyphian": 1322112, + "Pogian": 3212121, + "Aeraptian": 2121213, + "Epylian": 1212132, + "Gamian": 2121321, + "Kathian": 1213212, + "Lylian": 2132121, + "Epalian": 1321212, + "Eporian": 3211221, + "Rylian": 2112213, + "Epaptian": 1122132, + "Byrian": 1221321, + "Katanian": 2213211, + "Katyrian": 2132112, + "Rynian": 1321122, + "Korian": 3122112, + "Lynian": 1221123, + "Malian": 2211231, + "Synian": 2112312, + "Phragian": 1123122, + "Manian": 1231221, + "Marian": 2312211, + "Mycrian": 3121212, + "Ionorian": 1212123, + "Phrydian": 2121231, + "Zyptian": 1212312, + "Katothian": 2123121, + "Phrylian": 1231212, + "Kocrian": 2312121, + "Ionanian": 3121122, + "Aerothian": 1211223, + "Stagian": 2112231, + "Lothian": 1122312, + "Phrycrian": 1223121, + "Kyptian": 2231211, + "Ionylian": 2312112, + "Gydian": 4211112, + "Kogian": 2111124, + "Rarian": 1111242, + "Aerolian": 1112421, + "Karian": 1124211, + "Myptian": 1242111, + "Rydian": 2421111, + "Aeolynian": 2222211, + "Aeroptian": 2222112, + "Phryrian": 2221122, + "Gothian": 2211222, + "Storian": 2112222, + "Pyptian": 1122222, + "Thydian": 1222221, + "Aerycryllic": 22122111, + "Gadyllic": 21221112, + "Solyllic": 12211122, + "Zylyllic": 22111221, + "Mixodyllic": 21112212, + "Soryllic": 11122122, + "Godyllic": 11221221, + "Epiphyllic": 12212211, + "Pynyllic": 41112111, + "Bocryllic": 11121114, + "Kogyllic": 11211141, + "Raryllic": 12111411, + "Zycryllic": 21114111, + "Mycryllic": 11141112, + "Laptyllic": 11411121, + "Pylyllic": 14111211, + "Pothyllic": 32111211, + "Phronyllic": 21112113, + "Stynyllic": 11121132, + "Rathyllic": 11211321, + "Aeryptyllic": 12113211, + "Zydyllic": 21132111, + "Katolyllic": 11321112, + "Rythyllic": 13211121, + "Locryllic": 31131111, + "Bylyllic": 11311113, + "Sogyllic": 13111131, + "Ionycryllic": 31111311, + "Koptyllic": 11113113, + "Epyryllic": 11131131, + "Soptyllic": 11311311, + "Aeolylyllic": 13113111, + "Aeracryllic": 31122111, + "Epygyllic": 11221113, + "Thonyllic": 12211131, + "Lanyllic": 22111311, + "Phrynyllic": 21113112, + "Lycryllic": 11131122, + "Ionyptyllic": 11311221, + "Epathyllic": 13112211, + "Dydyllic": 31121211, + "Thogyllic": 11212113, + "Rygyllic": 12121131, + "Bycryllic": 21211311, + "Zacryllic": 12113112, + "Panyllic": 21131121, + "Dyryllic": 11311212, + "Zathyllic": 13112121, + "Dagyllic": 31121112, + "Katalyllic": 11211123, + "Katoryllic": 12111231, + "Dodyllic": 21112311, + "Zogyllic": 11123112, + "Madyllic": 11231121, + "Dycryllic": 12311211, + "Aeologyllic": 23112111, + "Sydyllic": 31113111, + "Katogyllic": 11131113, + "Zygyllic": 11311131, + "Aeralyllic": 13111311, + "Bacryllic": 31112211, + "Aerygyllic": 11122113, + "Dathyllic": 11221131, + "Boptyllic": 12211311, + "Bagyllic": 22113111, + "Mathyllic": 21131112, + "Styptyllic": 11311122, + "Zolyllic": 13111221, + "Rocryllic": 22212111, + "Zyryllic": 22121112, + "Sagyllic": 21211122, + "Epinyllic": 12111222, + "Katagyllic": 21112221, + "Ragyllic": 11122212, + "Gothyllic": 11222121, + "Lythyllic": 12221211, + "Ionocryllic": 22211121, + "Gocryllic": 22111212, + "Epiryllic": 21112122, + "Aeradyllic": 11121222, + "Staptyllic": 11212221, + "Danyllic": 12122211, + "Goptyllic": 21222111, + "Epocryllic": 12221112, + "Ionoptyllic": 22121121, + "Aeoloryllic": 21211212, + "Thydyllic": 12112122, + "Gycryllic": 21121221, + "Lyryllic": 11212212, + "Mogyllic": 12122121, + "Katodyllic": 21221211, + "Moptyllic": 12212112, + "Dolyllic": 41211111, + "Moryllic": 12111114, + "Bydyllic": 21111141, + "Pocryllic": 11111412, + "Phracryllic": 11114121, + "Gyryllic": 11141211, + "Phrygyllic": 11412111, + "Dogyllic": 14121111, + "Thagyllic": 41121111, + "Thoptyllic": 11211114, + "Phraptyllic": 12111141, + "Gylyllic": 21111411, + "Phralyllic": 11114112, + "Dygyllic": 11141121, + "Ronyllic": 11411211, + "Epogyllic": 14112111, + "Aeoladyllic": 41111211, + "Kocryllic": 11112114, + "Lodyllic": 11121141, + "Bynyllic": 11211411, + "Kydyllic": 12114111, + "Bygyllic": 21141111, + "Phryptyllic": 11411112, + "Ionayllic": 14111121, + "Phroryllic": 41111121, + "Thyphyllic": 11111214, + "Poptyllic": 11112141, + "Mixonyllic": 11121411, + "Paptyllic": 11214111, + "Storyllic": 12141111, + "Phrycryllic": 21411111, + "Palyllic": 14111112, + "Phranyllic": 32211111, + "Stydyllic": 22111113, + "Zadyllic": 21111132, + "Zalyllic": 11111322, + "Zocryllic": 11113221, + "Katocryllic": 11132211, + "Aerathyllic": 11322111, + "Stoptyllic": 13221111, + "Lydyllic": 32121111, + "Radyllic": 21211113, + "Stagyllic": 12111132, + "Ionoryllic": 21111321, + "Phrodyllic": 11113212, + "Aeragyllic": 11132121, + "Banyllic": 11321211, + "Epothyllic": 13212111, + "Zoryllic": 32112111, + "Phrolyllic": 21121113, + "Kolyllic": 11211132, + "Thodyllic": 12111321, + "Socryllic": 21113211, + "Aeolyllic": 11132112, + "Zythyllic": 11321121, + "Aeoryllic": 13211211, + "Mixolydyllic": 32111112, + "Mixonyphyllic": 21111123, + "Aeolanyllic": 11111232, + "Thocryllic": 11112321, + "Kygyllic": 11123211, + "Ionagyllic": 11232111, + "Gogyllic": 12321111, + "Phradyllic": 23211111, + "Ioniptyllic": 31311111, + "Kycryllic": 13111113, + "Aeolaptyllic": 31111131, + "Rodyllic": 11111313, + "Ionathyllic": 11113131, + "Pythyllic": 11131311, + "Zonyllic": 11313111, + "Ryryllic": 13131111, + "Aeolothyllic": 31221111, + "Ionyryllic": 12211113, + "Rydyllic": 22111131, + "Gonyllic": 21111312, + "Rolyllic": 11113122, + "Katydyllic": 11131221, + "Zyptyllic": 11312211, + "Modyllic": 13122111, + "Maptyllic": 31212111, + "Aeraptyllic": 12121113, + "Katadyllic": 21211131, + "Magyllic": 12111312, + "Phrylyllic": 21113121, + "Epigyllic": 11131212, + "Molyllic": 11312121, + "Ponyllic": 13121211, + "Thyptyllic": 31211211, + "Ionogyllic": 12112113, + "Aeolaryllic": 21121131, + "Katygyllic": 11211312, + "Ganyllic": 12113121, + "Kyptyllic": 21131211, + "Salyllic": 11312112, + "Sanyllic": 13121121, + "Doptyllic": 31211121, + "Ionilyllic": 12111213, + "Manyllic": 21112131, + "Polyllic": 11121312, + "Stanyllic": 11213121, + "Mixotharyllic": 12131211, + "Eporyllic": 21312111, + "Aerynyllic": 13121112, + "Lonyllic": 31121121, + "Sathyllic": 11211213, + "Layllic": 12112131, + "Saryllic": 21121311, + "Thacryllic": 11213112, + "Aeolynyllic": 12131121, + "Thadyllic": 21311211, + "Lynyllic": 13112112, + "Aeolathyllic": 31112121, + "Aeolocryllic": 11121213, + "Phroptyllic": 11212131, + "Kodyllic": 12121311, + "Epaptyllic": 21213111, + "Ionoyllic": 12131112, + "Gyptyllic": 21311121, + "Aerythyllic": 13111212, + "Zagyllic": 31112112, + "Epacryllic": 11121123, + "Thorcryllic": 11211231, + "Loptyllic": 12112311, + "Katylyllic": 21123111, + "Malyllic": 11231112, + "Mydyllic": 12311121, + "Thycryllic": 23111211, + "Gythyllic": 31111221, + "Pyryllic": 11112213, + "Rycryllic": 11122131, + "Phrathyllic": 11221311, + "Badyllic": 12213111, + "Phrocryllic": 22131111, + "Staryllic": 21311112, + "Zothyllic": 13111122, + "Tharyllic": 31111212, + "Sylyllic": 11112123, + "Lothyllic": 11121231, + "Daryllic": 11212311, + "Monyllic": 12123111, + "Styryllic": 21231111, + "Aeolacryllic": 12311112, + "Raptyllic": 23111121, + "Kataryllic": 31111122, + "Aerocryllic": 11111223, + "Zanyllic": 11112231, + "Aeolonyllic": 11122311, + "Aeonyllic": 11223111, + "Kyryllic": 12231111, + "Sythyllic": 22311111, + "Katycryllic": 23111112, + "Stogyllic": 22121211, + "Ionidyllic": 21212112, + "Stonyllic": 12121122, + "Stalyllic": 21211221, + "Poryllic": 12112212, + "Mocryllic": 21122121, + "Aeolyryllic": 11221212, + "Baryllic": 12212121, + "Dalyllic": 22112121, + "Ionyphyllic": 21121212, + "Zaptyllic": 11212122, + "Garyllic": 12121221, + "Gathyllic": 21212211, + "Mixopyryllic": 12122112, + "Ionacryllic": 21221121, + "Stylyllic": 12211212, + "Stycryllic": 42111111, + "Ionothyllic": 21111114, + "Mythyllic": 11111142, + "Aerylyllic": 11111421, + "Bonyllic": 11114211, + "Tholyllic": 11142111, + "Katyryllic": 11421111, + "Sadyllic": 14211111, + "Stolyllic": 41111112, + "Logyllic": 11111124, + "Dacryllic": 11111241, + "Thynyllic": 11112411, + "Gydyllic": 11124111, + "Eparyllic": 11241111, + "Dynyllic": 12411111, + "Ionyllic": 24111111, + "Zaryllic": 33111111, + "Dythyllic": 31111113, + "Ionaryllic": 11111133, + "Laryllic": 11111331, + "Kataptyllic": 11113311, + "Sonyllic": 11133111, + "Pathyllic": 11331111, + "Loryllic": 13311111, + "Aeronyllic": 32111121, + "Pycryllic": 21111213, + "Mygyllic": 11112132, + "Lylyllic": 11121321, + "Daptyllic": 11213211, + "Ioninyllic": 12132111, + "Epaphyllic": 21321111, + "Lolyllic": 13211112, + "Stacryllic": 31211112, + "Doryllic": 12111123, + "Kadyllic": 21111231, + "Rynyllic": 11112312, + "Aerogyllic": 11123121, + "Rothyllic": 11231211, + "Kagyllic": 12312111, + "Stathyllic": 23121111, + "Thyryllic": 22221111, + "Gygyllic": 22211112, + "Sodyllic": 22111122, + "Goryllic": 21111222, + "Bothyllic": 11112222, + "Gynyllic": 11122221, + "Ionaptyllic": 11222211, + "Phryryllic": 12222111, + "Racryllic": 22211211, + "Epicryllic": 22112112, + "Stygyllic": 21121122, + "Syryllic": 11211222, + "Stythyllic": 12112221, + "Aerothyllic": 21122211, + "Mixoryllic": 11222112, + "Thanyllic": 12221121, + "Roryllic": 22112211, + "Epotyllic": 21122112, + "Epidyllic": 11221122, + "Kaptyllic": 12211221, + "MajorDimin.": 21212121, + "MinorDimin.": 12121212, + "Aerycrygic": 221112111, + "Gadygic": 211121112, + "Solygic": 111211122, + "Zylygic": 112111221, + "Garygic": 121112211, + "Sorygic": 211122111, + "Godygic": 111221112, + "Epithygic": 112211121, + "Ionoptygic": 122111211, + "Kalygic": 311211111, + "Ionodygic": 112111113, + "Bythygic": 121111131, + "Epygic": 211111311, + "Marygic": 111113112, + "Gaptygic": 111131121, + "Aeroptygic": 111311211, + "Mylygic": 113112111, + "Galygic": 131121111, + "Mixolydygic": 311121111, + "Ionycrygic": 111211113, + "Zoptygic": 112111131, + "Phrygygic": 121111311, + "Locrygic": 211113111, + "Gonygic": 111131112, + "Aeracrygic": 111311121, + "Aerathygic": 113111211, + "Dorygic": 131112111, + "Dycrygic": 311112111, + "Aeolygic": 111121113, + "Dydygic": 111211131, + "Tholygic": 112111311, + "Rynygic": 121113111, + "Bycrygic": 211131111, + "Zacrygic": 111311112, + "Panygic": 113111121, + "Dyrygic": 131111211, + "Loptygic": 311111211, + "Katylygic": 111112113, + "Phradygic": 111121131, + "Mixodygic": 111211311, + "Katalygic": 112113111, + "Katorygic": 121131111, + "Dogygic": 211311111, + "Zodygic": 113111112, + "Madygic": 131111121, + "Bagygic": 221211111, + "Mathygic": 212111112, + "Styptygic": 121111122, + "Zolygic": 211111221, + "Sydygic": 111112212, + "Katygic": 111122121, + "Zyphygic": 111221211, + "Aeralygic": 112212111, + "Ryptygic": 122121111, + "Apinygic": 221111121, + "Katagygic": 211111212, + "Radygic": 111112122, + "Gothygic": 111121221, + "Lythygic": 111212211, + "Bacrygic": 112122111, + "Aerygic": 121221111, + "Dathygic": 212211111, + "Boptygic": 122111112, + "Epyrygic": 212112111, + "Aeradygic": 121121112, + "Staptygic": 211211121, + "Danygic": 112111212, + "Goptygic": 121112121, + "Epocrygic": 211121211, + "Rocrygic": 111212112, + "Zyrygic": 112121121, + "Sadygic": 121211211, + "Aeolorygic": 212111211, + "Thydygic": 121112112, + "Gycrygic": 211121121, + "Lyrygic": 111211212, + "Modygic": 112112121, + "Katodygic": 121121211, + "Moptygic": 211212111, + "Ionocrygic": 112121112, + "Gocrygic": 121211121, + "Manygic": 411111111, + "Polygic": 111111114, + "Stanygic": 111111141, + "Thaptygic": 111111411, + "Eporygic": 111114111, + "Aerynygic": 111141111, + "Thyptygic": 111411111, + "Ionogygic": 114111111, + "Aeolarygic": 141111111, + "Sathygic": 321111111, + "Ladygic": 211111113, + "Sarygic": 111111132, + "Thacrygic": 111111321, + "Aeolynygic": 111113211, + "Thadygic": 111132111, + "Lynygic": 111321111, + "Doptygic": 113211111, + "Ionilygic": 132111111, + "Phrygic": 312111111, + "Aeranygic": 121111113, + "Dothygic": 211111131, + "Lydygic": 111111312, + "Stadygic": 111113121, + "Byptygic": 111131211, + "Stodygic": 111312111, + "Zynygic": 113121111, + "Lonygic": 131211111, + "Zothygic": 311111121, + "Aeolathygic": 111111213, + "Aeolocrygic": 111112131, + "Phroptygic": 111121311, + "Kodygic": 111213111, + "Eparygic": 112131111, + "Ionygic": 121311111, + "Gyptygic": 213111111, + "Aerythygic": 131111112, + "Aeolacrygic": 311111112, + "Raptygic": 111111123, + "Gythygic": 111111231, + "Pyrygic": 111112311, + "Rycrygic": 111123111, + "Phrathygic": 111231111, + "Badygic": 112311111, + "Phrocrygic": 123111111, + "Starygic": 231111111, + "Kyrygic": 222111111, + "Sythygic": 221111112, + "Katycrygic": 211111122, + "Tharygic": 111111222, + "Sylygic": 111112221, + "Lothygic": 111122211, + "Darygic": 111222111, + "Monygic": 112221111, + "Styrygic": 122211111, + "Porygic": 221121111, + "Mocrygic": 211211112, + "Aeolyrygic": 112111122, + "Barygic": 121111221, + "Katarygic": 211112211, + "Aerocrygic": 111122112, + "Zanygic": 111221121, + "Aeolonygic": 112211211, + "Aeolanygic": 122112111, + "Kaptygic": 221111211, + "Sacrygic": 211112112, + "Padygic": 111121122, + "Epilygic": 111211221, + "Kynygic": 112112211, + "Stophygic": 121122111, + "Ionidygic": 211221111, + "Stonygic": 112211112, + "Stalygic": 122111121, + "Koptygic": 212121111, + "Raphygic": 121211112, + "Zycrygic": 212111121, + "Mycrygic": 121111212, + "Laptygic": 211112121, + "Pylygic": 111121212, + "Rodygic": 111212121, + "Epolygic": 112121211, + "Epidygic": 121212111, + "Phronygic": 211211211, + "Stynygic": 112112112, + "Zydygic": 121121121, + "Aerycryllian": 2111211111, + "Gadyllian": 1112111112, + "Solyllian": 1121111121, + "Zyphyllian": 1211111211, + "Garyllian": 2111112111, + "Soryllian": 1111121112, + "Godyllian": 1111211121, + "Epityllian": 1112111211, + "Ionyllian": 1121112111, + "Aeoryllian": 1211121111, + "Katoryllian": 3111111111, + "Dodyllian": 1111111113, + "Zogyllian": 1111111131, + "Madyllian": 1111111311, + "Dycryllian": 1111113111, + "Aeogyllian": 1111131111, + "Dydyllian": 1111311111, + "Thogyllian": 1113111111, + "Rygyllian": 1131111111, + "Bathyllian": 1311111111, + "Sydyllian": 2211111111, + "Katogyllian": 2111111112, + "Mixodyllian": 1111111122, + "Aeradyllian": 1111111221, + "Ryptyllian": 1111112211, + "Loptyllian": 1111122111, + "Kataphyllian": 1111221111, + "Phradyllian": 1112211111, + "Dagyllian": 1122111111, + "Katyllian": 1221111111, + "Gothyllian": 2121111111, + "Lythyllian": 1211111112, + "Bacryllian": 2111111121, + "Aerygyllian": 1111111212, + "Dathyllian": 1111112121, + "Boptyllian": 1111121211, + "Bagyllian": 1111212111, + "Mathyllian": 1112121111, + "Styptyllian": 1121211111, + "Zolyllian": 1212111111, + "Staptyllian": 2112111111, + "Danyllian": 1121111112, + "Goptyllian": 1211111121, + "Epocryllian": 2111111211, + "Rocryllian": 1111112112, + "Zyryllian": 1111121121, + "Sagyllian": 1111211211, + "Epinyllian": 1112112111, + "Katagyllian": 1121121111, + "Ragyllian": 1211211111, + "Thydyllian": 2111121111, + "Epiryllian": 1111211112, + "Lyryllian": 1112111121, + "Mogyllian": 1121111211, + "Katodyllian": 1211112111, + "Aerycratic": 21111111111, + "Monatic": 11111111112, + "Solatic": 11111111121, + "Zylatic": 11111111211, + "Mixolatic": 11111112111, + "Soratic": 11111121111, + "Godatic": 11111211111, + "Eptatic": 11112111111, + "Ionatic": 11121111111, + "Aeolatic": 11211111111, + "Thydatic": 12111111111, + "Chromatic": 111111111111, +} diff --git a/ziffers/mapper.py b/ziffers/mapper.py index ab668f4..364daa0 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -1,10 +1,11 @@ +""" Lark transformer for mapping Lark tokens to Ziffers objects """ from lark import Transformer from .classes import ( Ziffers, Whitespace, DurationChange, OctaveChange, - OctaveMod, + OctaveAdd, Pitch, RandomPitch, RandomPercent, @@ -25,157 +26,185 @@ from .classes import ( Euclid, RepeatedSequence, ) -from .common import ( - flatten, - sum_dict -) +from .common import flatten, sum_dict from .defaults import DEFAULT_DURS +# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name class ZiffersTransformer(Transformer): - - """ - Rules for transforming Ziffers expressions into tree. - """ + """Rules for transforming Ziffers expressions into tree.""" def start(self, items): + """Root for the rules""" seq = Sequence(values=items[0]) - return Ziffers(values=seq,options={}) + return Ziffers(values=seq, options={}) def sequence(self, items): + """Flatten sequence""" return flatten(items) - def random_integer(self, s): - val = s[0][1:-1].split(",") - return RandomInteger(min=val[0], max=val[1], text=s[0].value) + def random_integer(self, item): + """Parses random integer syntax""" + val = item[0][1:-1].split(",") + return RandomInteger(min=val[0], max=val[1], text=item[0].value) - def range(self, s): - val = s[0].split("..") - return Range(start=val[0], end=val[1], text=s[0]) + def range(self, item): + """Parses range syntax""" + val = item[0].split("..") + return Range(start=val[0], end=val[1], text=item[0]) def cycle(self, items): + """Parses cycle""" values = items[0] return Cyclic(values=values) - def pc(self, s): - if len(s) > 1: + def pitch_class(self, item): + """Parses pitch class""" + + # If there are prefixes + if len(item) > 1: # Collect&sum prefixes from any order: _qee^s4 etc. - result = sum_dict(s) + result = sum_dict(item) return Pitch(**result) - else: - val = s[0] - return Pitch(**val) - def pitch(self, s): - return {"pc": int(s[0].value), "text": s[0].value} + val = item[0] + return Pitch(**val) - def prefix(self, s): - return s[0] + def pitch(self, items): + """Return pitch class info""" + return {"pitch_class": int(items[0].value), "text": items[0].value} - def oct_change(self, s): - octave = s[0] - return [OctaveChange(value=octave["octave"], text=octave["text"]), s[1]] + def prefix(self, items): + """Return prefix""" + return items[0] - def oct_mod(self, s): - octave = s[0] - return [OctaveMod(value=octave["octave"], text=octave["text"]), s[1]] + def oct_change(self, items): + """Parses octave change""" + octave = items[0] + return [OctaveChange(value=octave["octave"], text=octave["text"]), items[1]] - def escaped_octave(self, s): - value = s[0][1:-1] - return {"octave": int(value), "text": s[0].value} + def oct_mod(self, items): + """Parses octave modification""" + octave = items[0] + return [OctaveAdd(value=octave["octave"], text=octave["text"]), items[1]] - def octave(self, s): - value = sum([1 if char == "^" else -1 for char in s[0].value]) - return {"octave": value, "text": s[0].value} + def escaped_octave(self, items): + """Return octave info""" + value = items[0][1:-1] + return {"octave": int(value), "text": items[0].value} - def chord(self, s): - return Chord(pcs=s, text="".join([val.text for val in s])) + def octave(self, items): + """Return octave info""" + value = sum(1 if char == "^" else -1 for char in items[0].value) + return {"octave": value, "text": items[0].value} - def dur_change(self, s): - durs = s[0] - return DurationChange(value=durs[1], text=durs[0]) + def chord(self, items): + """Parses chord""" + return Chord(pitch_classes=items, text="".join([val.text for val in items])) - def char_change(self, s): + def dur_change(self, items): + """Parses duration change""" + durs = items[0] + return DurationChange(value=durs["duration"], text=durs["text"]) + + def char_change(self, items): + """Return partial duration char info""" chars = "" durs = 0.0 - for (dchar, dots) in s: + for dchar, dots in items: val = DEFAULT_DURS[dchar] if dots > 0: val = val * (2.0 - (1.0 / (2 * dots))) chars = chars + (dchar + "." * dots) durs = durs + val - return [chars, durs] + return {"text":chars, "duration":durs} - def dchar_not_prefix(self, s): - dur = s[0].split(".", 1) + def dchar_not_prefix(self, items): + """Return partial duration char info""" + dur = items[0].split(".", 1) dots = 0 if len(dur) > 1: dots = len(dur[1]) + 1 return [dur[0], dots] - def escaped_decimal(self, s): - val = s[0] + def escaped_decimal(self, items): + """Return partial decimal info""" + val = items[0] val["text"] = "<" + val["text"] + ">" return val - def random_pitch(self, s): + def random_pitch(self, items): + """Parses random pitch""" return RandomPitch(text="?") - def random_percent(self, s): + def random_percent(self, items): + """Parses random percent""" return RandomPercent(text="%") - def duration_chars(self, s): - durations = [val[1] for val in s] - characters = "".join([val[0] for val in s]) + def duration_chars(self, items): + """Return partial duration info""" + durations = [val[1] for val in items] + characters = "".join([val[0] for val in items]) return {"duration": sum(durations), "text": characters} - def dotted_dur(self, s): - key = s[0] + def dotted_dur(self, items): + """Return partial duration info""" + key = items[0] val = DEFAULT_DURS[key] - dots = len(s) - 1 + dots = len(items) - 1 if dots > 0: val = val * (2.0 - (1.0 / (2 * dots))) return [key + "." * dots, val] - def decimal(self, s): - val = s[0] + def decimal(self, items): + """Return partial duration info""" + val = items[0] return {"duration": float(val), "text": val.value} - def dot(self, s): + def dot(self, items): + """Return partial duration info""" return "." - def dchar(self, s): - chardur = s[0].value + def dchar(self, items): + """Return partial duration info""" + chardur = items[0].value return chardur - def WS(self, s): - return Whitespace(text=s[0]) + def WS(self, items): + """Parse whitespace""" + return Whitespace(text=items[0]) def subdivision(self, items): + """Parse subdivision""" values = flatten(items[0]) return Subdivision( values=values, text="[" + "".join([val.text for val in values]) + "]" ) - def subitems(self, s): - return s + def subitems(self, items): + """Return subdivision items""" + return items # Eval rules - def eval(self, s): - val = s[0] + def eval(self, items): + """Parse eval""" + val = items[0] return Eval(values=val) - def operation(self, s): - return s + def operation(self, items): + """Return partial eval operations""" + return items - def atom(self, s): - val = s[0].value + def atom(self, token): + """Return partial eval item""" + val = token[0].value return Atom(value=val, text=val) # List rules def list(self, items): + """Parse list sequence notation, ex: (1 2 3)""" if len(items) > 1: prefixes = sum_dict(items[0:-1]) values = items[-1] @@ -187,9 +216,10 @@ class ZiffersTransformer(Transformer): return seq def repeated_list(self, items): + """Parse repeated list notation ex: (: 1 2 3 :)""" if len(items) > 2: prefixes = sum_dict(items[0:-2]) # If there are prefixes - if items[-1] != None: + if items[-1] is not None: seq = RepeatedListSequence( values=items[-2], repeats=items[-1], @@ -202,7 +232,7 @@ class ZiffersTransformer(Transformer): seq.update_values(prefixes) return seq else: - if items[-1] != None: + if items[-1] is not None: seq = RepeatedListSequence( values=items[-2], repeats=items[-1], @@ -214,54 +244,64 @@ class ZiffersTransformer(Transformer): ) return seq - def SIGNED_NUMBER(self, s): - val = s.value + def SIGNED_NUMBER(self, token): + """Parse integer""" + val = token.value return Integer(text=val, value=int(val)) - def number(self, s): - return s + def number(self, item): + """Return partial number (Integer or RandomInteger)""" + return item - def cyclic_number(self, s): - return Cyclic(values=s) + def cyclic_number(self, item): + """Parse cyclic notation""" + return Cyclic(values=item) - def lisp_operation(self, s): - op = s[0] - values = s[1:] + def lisp_operation(self, items): + """Parse lisp like list operation""" + op = items[0] + values = items[1:] return Operation( operator=op, values=values, text="(+" + "".join([v.text for v in values]) + ")", ) - def operator(self, s): - val = s[0].value + def operator(self, token): + """Parse operator""" + val = token[0].value return Operator(text=val) - def list_items(self, s): - return Sequence(values=s) + def list_items(self, items): + """Parse sequence""" + return Sequence(values=items) - def list_op(self, s): - return ListOperation(values=s) + def list_op(self, items): + """Parse list operation""" + return ListOperation(values=items) - 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] + def euclid(self, items): + """Parse euclid notation""" + params = items[1][1:-1].split(",") + init = {"onset": items[0], "pulses": params[0], "length": params[1]} + text = items[0].text + items[1] if len(params) > 2: init["rotate"] = params[2] - if len(s) > 2: - init["offset"] = s[2] - text = text + s[2].text + if len(items) > 2: + init["offset"] = items[2] + text = text + items[2].text init["text"] = text return Euclid(**init) - def euclid_operator(self, s): - return s.value + def euclid_operator(self, token): + """Return euclid operators""" + return token.value - def repeat(self, s): - if s[-1] != None: + def repeat(self, items): + """Parse repeated sequence, ex: [: 1 2 3 :]""" + if items[-1] is not None: return RepeatedSequence( - values=s[0], repeats=s[-1], wrap_end=":" + s[-1].text + "]" + values=items[0], repeats=items[-1], wrap_end=":" + items[-1].text + "]" ) else: - return RepeatedSequence(values=s[0], repeats=Integer(value=1, text="1")) + return RepeatedSequence(values=items[0], repeats=Integer(value=1, text="1")) diff --git a/ziffers/parser.py b/ziffers/parser.py index 4176de2..c162111 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -1,7 +1,9 @@ -from rich import print -from .mapper import * +""" Module for the parser """ from pathlib import Path from lark import Lark +from .classes import Ziffers +from .mapper import ZiffersTransformer + grammar_path = Path(__file__).parent grammar = grammar_path / "ziffers.lark" @@ -14,45 +16,83 @@ ziffers_parser = Lark.open( transformer=ZiffersTransformer(), ) -def parse_expression(expr: str): - """Parse an expression using the Ziffers parser""" + +def parse_expression(expr: str) -> Ziffers: + """Parse an expression using the Ziffers parser + + Args: + expr (str): Ziffers expression as a string + + Returns: + Ziffers: Reutrns Ziffers iterable + """ return ziffers_parser.parse(expr) -def zparse(expr: str, opts: dict=None): + +def zparse(expr: str, opts: dict = None) -> Ziffers: + """Parses ziffers expression with options + + Args: + expr (str): Ziffers expression as a string + opts (dict, optional): Options for parsing the Ziffers expression. Defaults to None. + + Returns: + Ziffers: Returns Ziffers iterable parsed with the given options + """ 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) +# pylint: disable=invalid-name -def z3(expr: str, opts: dict=None): - return zparse(expr,opts) -def z4(expr: str, opts: dict=None): - return zparse(expr,opts) +def z0(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + 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 z1(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + 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 z2(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + return zparse(expr, opts) -def z9(expr: str, opts: dict=None): - return zparse(expr,opts) \ No newline at end of file + +def z3(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + return zparse(expr, opts) + + +def z4(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + return zparse(expr, opts) + + +def z5(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + return zparse(expr, opts) + + +def z6(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + return zparse(expr, opts) + + +def z7(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + return zparse(expr, opts) + + +def z8(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + return zparse(expr, opts) + + +def z9(expr: str, opts: dict = None) -> Ziffers: + """Shortened method name for zparse""" + return zparse(expr, opts) diff --git a/ziffers/scale.py b/ziffers/scale.py index 418ae0b..6541ae8 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -1,1528 +1,42 @@ +""" Methods for calculating notes from scales and list of all intervals in scales""" #!/usr/bin/env python3 - +# pylint: disable=locally-disabled, no-name-in-module import re from math import floor - -SCALES = { - "Minoric": 444, - "Thaptic": 4341, - "Lothic": 3414, - "Phratic": 4143, - "Aerathic": 1434, - "Epathic": 4323, - "Mynic": 3234, - "Rothic": 2343, - "Eporic": 3432, - "Zyphic": 4431, - "Epogic": 4314, - "Lanic": 3144, - "Pyrric": 1443, - "Aeoloric": 4413, - "Gonic": 4134, - "Dalic": 1344, - "Dygic": 3441, - "Daric": 4332, - "Lonic": 3324, - "Phradic": 3243, - "Bolic": 2433, - "Saric": 4233, - "Zoptic": 2334, - "Aeraphic": 3342, - "Byptic": 3423, - "Aeolic": 4422, - "Koptic": 4224, - "Mixolyric": 2244, - "Lydic": 2442, - "Stathic": 4242, - "Dadic": 2424, - "Phrynic": 3333, - "Epathitonic": 32322, - "Mynitonic": 23223, - "Rocritonic": 32232, - "Pentatonic": 22323, - "Thaptitonic": 23232, - "Magitonic": 43221, - "Daditonic": 32214, - "Aeolyphritonic": 22143, - "Gycritonic": 21432, - "Pyritonic": 14322, - "Gathitonic": 42321, - "Ionitonic": 23214, - "Phrynitonic": 32142, - "Stathitonic": 21423, - "Thalitonic": 14232, - "Zolitonic": 42141, - "Epogitonic": 21414, - "Lanitonic": 14142, - "Paptitonic": 41421, - "Ionacritonic": 14214, - "Phraditonic": 41412, - "Aeoloritonic": 14124, - "Gonitonic": 41241, - "Dalitonic": 12414, - "Dygitonic": 24141, - "Aeracritonic": 41232, - "Byptitonic": 12324, - "Daritonic": 23241, - "Lonitonic": 32412, - "Ionycritonic": 24123, - "Lothitonic": 41223, - "Phratonic": 12234, - "Aerathitonic": 22341, - "Saritonic": 23412, - "Zoptitonic": 34122, - "Dolitonic": 44121, - "Poritonic": 41214, - "Aerylitonic": 12144, - "Zagitonic": 21441, - "Lagitonic": 14412, - "Molitonic": 43311, - "Staptitonic": 33114, - "Mothitonic": 31143, - "Aeritonic": 11433, - "Ragitonic": 14331, - "Ionaditonic": 43212, - "Bocritonic": 32124, - "Gythitonic": 21243, - "Pagitonic": 12432, - "Aeolythitonic": 24321, - "Zacritonic": 43131, - "Laritonic": 31314, - "Thacritonic": 13143, - "Styditonic": 31431, - "Loritonic": 14313, - "Aeolyritonic": 43113, - "Goritonic": 31134, - "Aeoloditonic": 11343, - "Doptitonic": 13431, - "Aeraphitonic": 34311, - "Zathitonic": 42411, - "Raditonic": 24114, - "Stonitonic": 41142, - "Syptitonic": 11424, - "Ionythitonic": 14241, - "Aeolanitonic": 42231, - "Danitonic": 22314, - "Ionaritonic": 23142, - "Dynitonic": 31422, - "Zyditonic": 14223, - "Aeolacritonic": 42123, - "Zythitonic": 21234, - "Dyritonic": 12342, - "Koptitonic": 23421, - "Thocritonic": 34212, - "Lycritonic": 41331, - "Daptitonic": 13314, - "Kygitonic": 33141, - "Mocritonic": 31413, - "Zynitonic": 14133, - "Epygitonic": 41322, - "Zaptitonic": 13224, - "Kagitonic": 32241, - "Zogitonic": 22413, - "Epyritonic": 24132, - "Zothitonic": 41313, - "Phrolitonic": 13134, - "Ionagitonic": 31341, - "Aeolapritonic": 13413, - "Kyritonic": 34131, - "Ionyptitonic": 41133, - "Gyritonic": 11334, - "Zalitonic": 13341, - "Stolitonic": 33411, - "Bylitonic": 34113, - "Thoditonic": 33231, - "Dogitonic": 32313, - "Phralitonic": 23133, - "Garitonic": 31332, - "Soptitonic": 13323, - "Kataritonic": 33222, - "Sylitonic": 32223, - "Thonitonic": 22233, - "Phropitonic": 22332, - "Staditonic": 23322, - "Lyditonic": 33132, - "Mythitonic": 31323, - "Sogitonic": 13233, - "Gothitonic": 32331, - "Rothitonic": 23313, - "Zylitonic": 44211, - "Zoditonic": 42114, - "Zaritonic": 21144, - "Phrythitonic": 11442, - "Rolitonic": 14421, - "Ranitonic": 44112, - "Laditonic": 41124, - "Poditonic": 11244, - "Ionothitonic": 12441, - "Kanitonic": 24411, - "Ryphitonic": 43122, - "Gylitonic": 31224, - "Aeolycritonic": 12243, - "Pynitonic": 22431, - "Zanitonic": 24312, - "Phronitonic": 42312, - "Banitonic": 23124, - "Aeronitonic": 31242, - "Golitonic": 12423, - "Dyptitonic": 24231, - "Aerynitonic": 42213, - "Palitonic": 22134, - "Stothitonic": 21342, - "Aerophitonic": 13422, - "Katagitonic": 34221, - "Ionoditonic": 42132, - "Bogitonic": 21324, - "Mogitonic": 13242, - "Docritonic": 32421, - "Epaditonic": 24213, - "Mixitonic": 33321, - "Phrothitonic": 33213, - "Katycritonic": 32133, - "Ionalitonic": 21333, - "Loptitonic": 13332, - "Thyritonic": 33312, - "Thoptitonic": 33123, - "Bycritonic": 31233, - "Pathitonic": 12333, - "Myditonic": 23331, - "Bolitonic": 42222, - "Bothitonic": 22224, - "Kataditonic": 22242, - "Koditonic": 22422, - "Tholitonic": 24222, - "Epathimic": 322122, - "Mynimic": 221223, - "Rocrimic": 212232, - "Eporimic": 122322, - "Thaptimic": 223221, - "Lothimic": 232212, - "Dyrimic": 421221, - "Koptimic": 212214, - "Thocrimic": 122142, - "Aeolanimic": 221421, - "Danimic": 214212, - "Ionarimic": 142122, - "Daptimic": 414111, - "Kygimic": 141114, - "Mocrimic": 411141, - "Zynimic": 111414, - "Aeolimic": 114141, - "Zythimic": 141411, - "Epygimic": 412311, - "Zaptimic": 123114, - "Kagimic": 231141, - "Zogimic": 311412, - "Epyrimic": 114123, - "Lycrimic": 141231, - "Bylimic": 412221, - "Zothimic": 122214, - "Phrolimic": 222141, - "Ionagimic": 221412, - "Aeolaphimic": 214122, - "Kycrimic": 141222, - "Garimic": 412212, - "Soptimic": 122124, - "Ionyptimic": 221241, - "Gyrimic": 212412, - "Zalimic": 124122, - "Stolimic": 241221, - "Thonimic": 411411, - "Stadimic": 114114, - "Thodimic": 141141, - "Mythimic": 411321, - "Sogimic": 113214, - "Gogimic": 132141, - "Rothimic": 321411, - "Katarimic": 214113, - "Sylimic": 141132, - "Mixolimic": 323211, - "Dadimic": 232113, - "Aeolyphimic": 321132, - "Gycrimic": 211323, - "Pyrimic": 113232, - "Lydimic": 132321, - "Ionacrimic": 323112, - "Gathimic": 231123, - "Ionynimic": 311232, - "Phrynimic": 112323, - "Stathimic": 123231, - "Thatimic": 232311, - "Dalimic": 322311, - "Dygimic": 223113, - "Zolimic": 231132, - "Epogimic": 311322, - "Lanimic": 113223, - "Paptimic": 132231, - "Darmic": 322212, - "Lonimic": 222123, - "Ionycrimic": 221232, - "Phradimic": 212322, - "Aeolorimic": 123222, - "Gonimic": 232221, - "Phracrimic": 321222, - "Aerathimic": 212223, - "Sarimic": 122232, - "Zoptimic": 222321, - "Zeracrimic": 223212, - "Byptimic": 232122, - "Starimic": 432111, - "Phrathimic": 321114, - "Saptimic": 211143, - "Aerodimic": 111432, - "Macrimic": 114321, - "Rogimic": 143211, - "Bygimic": 431121, - "Thycrimic": 311214, - "Aeoladimic": 112143, - "Dylimic": 121431, - "Eponimic": 214311, - "Katygimic": 143112, - "Stalimic": 423111, - "Stoptimic": 231114, - "Zygimic": 311142, - "Kataptimic": 111423, - "Aeolaptimic": 114231, - "Pothimic": 142311, - "Rycrimic": 422121, - "Ronimic": 221214, - "Stycrimic": 212142, - "Katorimic": 121422, - "Epythimic": 214221, - "Kaptimic": 142212, - "Katythimic": 421311, - "Madimic": 213114, - "Aerygimic": 131142, - "Pylimic": 311421, - "Ionathimic": 114213, - "Morimic": 142131, - "Aerycrimic": 421131, - "Ganimic": 211314, - "Eparimic": 113142, - "Lyrimic": 131421, - "Phraptimic": 314211, - "Bacrimic": 142113, - "Phralimic": 413211, - "Phrogimic": 132114, - "Rathimic": 321141, - "Katocrimic": 211413, - "Phryptimic": 114132, - "Katynimic": 141321, - "Solimic": 413121, - "Ionolimic": 131214, - "Ionophimic": 312141, - "Aeologimic": 121413, - "Zadimic": 214131, - "Sygimic": 141312, - "Thogimic": 413112, - "Rythimic": 131124, - "Donimic": 311241, - "Aeoloptimic": 112413, - "Panimic": 124131, - "Lodimic": 241311, - "Laptimic": 412131, - "Lygimic": 121314, - "Logimic": 213141, - "Lalimic": 131412, - "Sothimic": 314121, - "Phrocrimic": 141213, - "Modimic": 412122, - "Barimic": 121224, - "Poptimic": 212241, - "Sagimic": 122412, - "Aelothimic": 224121, - "Socrimic": 241212, - "Syrimic": 412113, - "Stodimic": 121134, - "Ionocrimic": 211341, - "Zycrimic": 113412, - "Ionygimic": 134121, - "Katathimic": 341211, - "Bolimic": 411312, - "Bothimic": 113124, - "Katadimic": 131241, - "Kodimic": 312411, - "Tholimic": 124113, - "Ralimic": 241131, - "Kanimic": 411231, - "Zylimic": 112314, - "Zodimic": 123141, - "Zarimic": 231411, - "Phrythimic": 314112, - "Rorimic": 141123, - "Pynimic": 411132, - "Zanimic": 111324, - "Ranimic": 113241, - "Ladimic": 132411, - "Podimic": 324111, - "Ionothimic": 241113, - "Kytrimic": 411123, - "Golimic": 111234, - "Dyptimic": 112341, - "Ryrimic": 123411, - "Gylimic": 234111, - "Aeolycrimic": 341112, - "Palimic": 332211, - "Stothimic": 322113, - "Aeronimic": 221133, - "Katagimic": 211332, - "Phronimic": 113322, - "Banimic": 133221, - "Ionodimic": 331311, - "Bogimic": 313113, - "Mogimic": 131133, - "Docrimic": 311331, - "Epadimic": 113313, - "Aerynimic": 133131, - "Mydimic": 331131, - "Thyptimic": 311313, - "Phrothimic": 113133, - "Katycrimic": 131331, - "Ionalimic": 313311, - "Loptimic": 133113, - "Zagimic": 331122, - "Lagimic": 311223, - "Thyrimic": 112233, - "Thothimic": 122331, - "Bycrimic": 223311, - "Pathimic": 233112, - "Mothimic": 322131, - "Aeranimic": 221313, - "Ragimic": 213132, - "Dolimic": 131322, - "Porimic": 313221, - "Aerylimic": 132213, - "Bocrimic": 321312, - "Gythimic": 213123, - "Pagimic": 131232, - "Aeolythimic": 312321, - "Molimic": 123213, - "Staptimic": 232131, - "Zacrimic": 321231, - "Larimic": 212313, - "Thacrimic": 123132, - "Stydimic": 231321, - "Lorimic": 313212, - "Ionadimic": 132123, - "Ionythimic": 313131, - "Aerythimic": 131313, - "Dynimic": 313122, - "Zydimic": 131223, - "Zathimic": 312231, - "Radimic": 122313, - "Stonimic": 223131, - "Syptimic": 231312, - "Ponimic": 441111, - "Kadimic": 411114, - "Gynimic": 111144, - "Thydimic": 111441, - "Polimic": 114411, - "Thanimic": 144111, - "Lathimic": 431211, - "Aeralimic": 312114, - "Kynimic": 121143, - "Stynimic": 211431, - "Epytimic": 114312, - "Katoptimic": 143121, - "Galimic": 431112, - "Kathimic": 311124, - "Lylimic": 111243, - "Epalimic": 112431, - "Epacrimic": 124311, - "Sathimic": 243111, - "Katanimic": 422211, - "Katyrimic": 222114, - "Rynimic": 221142, - "Pogimic": 211422, - "Aeraptimic": 114222, - "Epylimic": 142221, - "Manimic": 421212, - "Marimic": 212124, - "Locrimic": 121242, - "Rylimic": 212421, - "Epatimic": 124212, - "Byrimic": 242121, - "Kocrimic": 421113, - "Korimic": 211134, - "Lynimic": 111342, - "Malimic": 113421, - "Synimic": 134211, - "Phragimic": 342111, - "Mycrimic": 411222, - "Ionorimic": 112224, - "Phrydimic": 122241, - "Zyptimic": 222411, - "Katothimic": 224112, - "Phrylimic": 241122, - "Aerothimic": 411213, - "Stagimic": 112134, - "Dorimic": 121341, - "Phrycrimic": 213411, - "Kyptimic": 134112, - "Ionylimic": 341121, - "Epynimic": 333111, - "Ionogimic": 331113, - "Kydimic": 311133, - "Gaptimic": 111333, - "Tharimic": 113331, - "Ionaphimic": 133311, - "Thoptimic": 332121, - "Bagimic": 321213, - "Kyrimic": 212133, - "Sonimic": 121332, - "Aeolonimic": 213321, - "Rygimic": 133212, - "Thagimic": 332112, - "Kolimic": 321123, - "Dycrimic": 211233, - "Epycrimic": 112332, - "Gocrimic": 123321, - "Katolimic": 233211, - "Dagimic": 331221, - "Aeolydimic": 312213, - "Parimic": 122133, - "Ionaptimic": 221331, - "Thylimic": 213312, - "Lolimic": 133122, - "Thalimic": 331212, - "Stygimic": 312123, - "Aeolygimic": 121233, - "Aerogimic": 212331, - "Dacrimic": 123312, - "Baptimic": 233121, - "Stythimic": 323121, - "Kothimic": 231213, - "Pygimic": 312132, - "Rodimic": 121323, - "Sorimic": 213231, - "Monimic": 132312, - "Aeragimic": 322221, - "Epothimic": 222213, - "Salimic": 222132, - "Lyptimic": 221322, - "Katonimic": 213222, - "Gygimic": 132222, - "Aeradimic": 321321, - "Zyrimic": 213213, - "Stylimic": 132132, - "Lythimic": 312312, - "Dodimic": 123123, - "Katalimic": 231231, - "Boptimic": 312222, - "Stogimic": 122223, - "Thynimic": 222231, - "Aeolathimic": 222312, - "Bythimic": 223122, - "Padimic": 231222, - "Dathimic": 422112, - "Epagimic": 221124, - "Raptimic": 211242, - "Epolimic": 112422, - "Sythimic": 124221, - "Sydimic": 242211, - "Gacrimic": 421122, - "Borimic": 211224, - "Sycrimic": 112242, - "Gadimic": 122421, - "Aeolocrimic": 224211, - "Phrygimic": 242112, - "WholeTone": 222222, - "Lydian": 2221221, - "Mixolydian": 2212212, - "Aeolian": 2122122, - "Locrian": 1221222, - "Ionian": 2212221, - "Dorian": 2122212, - "Phrygian": 1222122, - "Ionythian": 4122111, - "Aeolyrian": 1221114, - "Gorian": 2211141, - "Aeolodian": 2111412, - "Doptian": 1114122, - "Aeraphian": 1141221, - "Zacrian": 1412211, - "Ionarian": 4113111, - "Dynian": 1131114, - "Zydian": 1311141, - "Zathian": 3111411, - "Radian": 1114113, - "Stonian": 1141131, - "Syptian": 1411311, - "Aeolacrian": 4111311, - "Zythian": 1113114, - "Dyrian": 1131141, - "Koptian": 1311411, - "Thocrian": 3114111, - "Aeolanian": 1141113, - "Danian": 1411131, - "Zogian": 4111221, - "Epyrian": 1112214, - "Lycrian": 1122141, - "Daptian": 1221411, - "Kygian": 2214111, - "Mocrian": 2141112, - "Zynian": 1411122, - "Phrolian": 3221211, - "Ionagian": 2212113, - "Aeodian": 2121132, - "Kycrian": 1211322, - "Epygian": 2113221, - "Zaptian": 1132212, - "Kagian": 1322121, - "Soptian": 3221112, - "Ionyptian": 2211123, - "Gyrian": 2111232, - "Zalian": 1112322, - "Stolian": 1123221, - "Bylian": 1232211, - "Zothian": 2322111, - "Thonian": 3212211, - "Phrorian": 2122113, - "Stadian": 1221132, - "Thodian": 2211321, - "Dogian": 2113212, - "Mixopyrian": 1132122, - "Garian": 1321221, - "Epathian": 3211311, - "Mythian": 2113113, - "Sogian": 1131132, - "Gogian": 1311321, - "Rothian": 3113211, - "Katarian": 1132113, - "Stylian": 1321131, - "Stathian": 3211122, - "Mixonyphian": 2111223, - "Magian": 1112232, - "Dadian": 1122321, - "Aeolylian": 1223211, - "Gycrian": 2232111, - "Pyrian": 2321112, - "Epogian": 3113112, - "Lanian": 1131123, - "Paptian": 1311231, - "Ionacrian": 3112311, - "Gathian": 1123113, - "Ionyphian": 1231131, - "Phrynian": 2311311, - "Ionycrian": 3112212, - "Phradian": 1122123, - "Aeolorian": 1221231, - "Gonian": 2212311, - "Dalian": 2123112, - "Dygian": 1231122, - "Zolian": 2311221, - "Aerathian": 3112122, - "Sarian": 1121223, - "Zoptian": 1212231, - "Aeracrian": 2122311, - "Byptian": 1223112, - "Darian": 2231121, - "Lonian": 2311212, - "Aeopian": 4212111, - "Rygian": 2121114, - "Epynian": 1211142, - "Ionogian": 2111421, - "Kydian": 1114212, - "Gaptian": 1142121, - "Tharian": 1421211, - "Epycrian": 4211121, - "Gocrian": 2111214, - "Katolian": 1112142, - "Thoptian": 1121421, - "Bagian": 1214211, - "Kyrian": 2142111, - "Sonian": 1421112, - "Parian": 4131111, - "Ionaptian": 1311114, - "Thylian": 3111141, - "Lolian": 1111413, - "Thagian": 1114131, - "Kolian": 1141311, - "Dycrian": 1413111, - "Stygian": 4121211, - "Aeolygian": 1212114, - "Aerogian": 2121141, - "Dacrian": 1211412, - "Baptian": 2114121, - "Dagian": 1141212, - "Aeolydian": 1412121, - "Stythian": 4121121, - "Kothian": 1211214, - "Pygian": 2112141, - "Rodian": 1121412, - "Sorian": 1214121, - "Monian": 2141211, - "Thalian": 1412112, - "Zorian": 4121112, - "Aeragian": 1211124, - "Epothian": 2111241, - "Salian": 1112412, - "Lyptian": 1124121, - "Katonian": 1241211, - "Gyphian": 2412111, - "Thacrian": 4112211, - "Dodian": 1122114, - "Aeolyptian": 1221141, - "Aeolonian": 2211411, - "Aeradian": 2114112, - "Aeolagian": 1141122, - "Zyrian": 1411221, - "Aeolathian": 4112121, - "Bythian": 1121214, - "Padian": 1212141, - "Rolian": 2121411, - "Pydian": 1214112, - "Thygian": 2141121, - "Katalian": 1411212, - "Saptian": 4111212, - "Aerodian": 1112124, - "Macrian": 1121241, - "Rogian": 1212411, - "Boptian": 2124111, - "Stogian": 1241112, - "Thynian": 2411121, - "Thycrian": 4111131, - "Aeoladian": 1111314, - "Dylian": 1113141, - "Eponian": 1131411, - "Katygian": 1314111, - "Starian": 3141111, - "Phrathian": 1411113, - "Stalian": 3311211, - "Stoptian": 3112113, - "Zygian": 1121133, - "Kataptian": 1211331, - "Aeolaptian": 2113311, - "Pothian": 1133112, - "Bygian": 1331121, - "Morian": 3231111, - "Rycrian": 2311113, - "Ronian": 3111132, - "Stycrian": 1111323, - "Katorian": 1113231, - "Epythian": 1132311, - "Kaptian": 1323111, - "Phraptian": 3222111, - "Bacrian": 2221113, - "Katythian": 2211132, - "Madian": 2111322, - "Aerygian": 1113222, - "Pylian": 1132221, - "Ionathian": 1322211, - "Katocrian": 3213111, - "Phryptian": 2131113, - "Katynian": 1311132, - "Aerycrian": 3111321, - "Ganian": 1113213, - "Eparian": 1132131, - "Lyrian": 1321311, - "Ionopian": 3212112, - "Aeologian": 2121123, - "Zadian": 1211232, - "Sygian": 2112321, - "Phralian": 1123212, - "Phrogian": 1232121, - "Rathian": 2321211, - "Rythian": 3211212, - "Donian": 2112123, - "Aeoloptian": 1121232, - "Panian": 1212321, - "Lodian": 2123211, - "Solian": 1232112, - "Ionolian": 2321121, - "Laptian": 3211131, - "Lygian": 2111313, - "Logian": 1113132, - "Lalian": 1131321, - "Sothian": 1313211, - "Phrocrian": 3132111, - "Thogian": 1321113, - "Katathian": 3131211, - "Modian": 1312113, - "Barian": 3121131, - "Mixolocrian": 1211313, - "Sagian": 2113131, - "Aeolothian": 1131312, - "Socrian": 1313121, - "Tholian": 3131121, - "Ralian": 1311213, - "Syrian": 3112131, - "Stodian": 1121313, - "Ionocrian": 1213131, - "Zycrian": 2131311, - "Ionygian": 1313112, - "Zarian": 3131112, - "Phrythian": 1311123, - "Rorian": 3111231, - "Bolian": 1112313, - "Bothian": 1123131, - "Katadian": 1231311, - "Kodian": 2313111, - "Ranian": 3123111, - "Ladian": 1231113, - "Podian": 2311131, - "Ionothian": 3111312, - "Kanian": 1113123, - "Zylian": 1131231, - "Zodian": 1312311, - "Golian": 3122211, - "Dyptian": 1222113, - "Ryphian": 2221131, - "Gylian": 2211312, - "Aeolycrian": 2113122, - "Pynian": 1131222, - "Zanian": 1312221, - "Palian": 3122121, - "Stothian": 1221213, - "Aerorian": 2212131, - "Katagian": 2121312, - "Phronian": 1213122, - "Banian": 2131221, - "Aeronian": 1312212, - "Loptian": 3121311, - "Ionodian": 1213113, - "Bogian": 2131131, - "Mogian": 1311312, - "Docrian": 3113121, - "Epadian": 1131213, - "Aerynian": 1312131, - "Bycrian": 3121221, - "Pathian": 1212213, - "Mydian": 2122131, - "Thyptian": 1221312, - "Phrothian": 2213121, - "Katycrian": 2131212, - "Ionalian": 1312122, - "Dolian": 3112221, - "Porian": 1122213, - "Aerylian": 1222131, - "Zagian": 2221311, - "Lagian": 2213112, - "Tyrian": 2131122, - "Mixonorian": 1311222, - "Pagian": 3111222, - "Aeolythian": 1112223, - "Molian": 1122231, - "Staptian": 1222311, - "Mothian": 2223111, - "Aeranian": 2231112, - "Ragian": 2311122, - "Larian": 2222121, - "Lythian": 2221212, - "Stydian": 2212122, - "Lorian": 2121222, - "Ionadian": 1212222, - "Bocrian": 2122221, - "Mixolythian": 1222212, - "Thadian": 4311111, - "Sanian": 3111114, - "Ionydian": 1111143, - "Epydian": 1111431, - "Katydian": 1114311, - "Mathian": 1143111, - "Aeryptian": 1431111, - "Pythian": 4221111, - "Katylian": 2211114, - "Bydian": 2111142, - "Bynian": 1111422, - "Galian": 1114221, - "Zonian": 1142211, - "Myrian": 1422111, - "Katogian": 4211211, - "Stacrian": 2112114, - "Styrian": 1121142, - "Ionyrian": 1211421, - "Phrodian": 2114211, - "Pycrian": 1142112, - "Gyptian": 1421121, - "Katacrian": 4112112, - "Sodian": 1121124, - "Bathian": 1211241, - "Mylian": 2112411, - "Godian": 1124112, - "Thorian": 1241121, - "Zocrian": 2411211, - "Stanian": 4111122, - "Epanian": 1111224, - "Konian": 1112241, - "Stocrian": 1122411, - "Kalian": 1224111, - "Phroptian": 2241111, - "Dydian": 2411112, - "Katyptian": 4111113, - "Epodian": 1111134, - "Mygian": 1111341, - "Pacrian": 1113411, - "Aerocrian": 1134111, - "Aeolarian": 1341111, - "Kythian": 3411111, - "Bonian": 3321111, - "Badian": 3211113, - "Katodian": 2111133, - "Sadian": 1111332, - "Dothian": 1113321, - "Moptian": 1133211, - "Aeryrian": 1332111, - "Epagian": 3312111, - "Raptian": 3121113, - "Epolian": 1211133, - "Sythian": 2111331, - "Sydian": 1113312, - "Epocrian": 1133121, - "Kylian": 1331211, - "Gacrian": 3311121, - "Borian": 3111213, - "Sycrian": 1112133, - "Gadian": 1121331, - "Aeolocrian": 1213311, - "Mixodorian": 2133111, - "Dathian": 1331112, - "Katoptian": 3311112, - "Ponian": 3111123, - "Kadian": 1111233, - "Gynian": 1112331, - "Thyphian": 1123311, - "Polian": 1233111, - "Thanian": 2331111, - "Epacrian": 3221121, - "Sathian": 2211213, - "Lathian": 2112132, - "Aeralian": 1121322, - "Kynian": 1213221, - "Stynian": 2132211, - "Epyphian": 1322112, - "Pogian": 3212121, - "Aeraptian": 2121213, - "Epylian": 1212132, - "Gamian": 2121321, - "Kathian": 1213212, - "Lylian": 2132121, - "Epalian": 1321212, - "Eporian": 3211221, - "Rylian": 2112213, - "Epaptian": 1122132, - "Byrian": 1221321, - "Katanian": 2213211, - "Katyrian": 2132112, - "Rynian": 1321122, - "Korian": 3122112, - "Lynian": 1221123, - "Malian": 2211231, - "Synian": 2112312, - "Phragian": 1123122, - "Manian": 1231221, - "Marian": 2312211, - "Mycrian": 3121212, - "Ionorian": 1212123, - "Phrydian": 2121231, - "Zyptian": 1212312, - "Katothian": 2123121, - "Phrylian": 1231212, - "Kocrian": 2312121, - "Ionanian": 3121122, - "Aerothian": 1211223, - "Stagian": 2112231, - "Lothian": 1122312, - "Phrycrian": 1223121, - "Kyptian": 2231211, - "Ionylian": 2312112, - "Gydian": 4211112, - "Kogian": 2111124, - "Rarian": 1111242, - "Aerolian": 1112421, - "Karian": 1124211, - "Myptian": 1242111, - "Rydian": 2421111, - "Aeolynian": 2222211, - "Aeroptian": 2222112, - "Phryrian": 2221122, - "Gothian": 2211222, - "Storian": 2112222, - "Pyptian": 1122222, - "Thydian": 1222221, - "Aerycryllic": 22122111, - "Gadyllic": 21221112, - "Solyllic": 12211122, - "Zylyllic": 22111221, - "Mixodyllic": 21112212, - "Soryllic": 11122122, - "Godyllic": 11221221, - "Epiphyllic": 12212211, - "Pynyllic": 41112111, - "Bocryllic": 11121114, - "Kogyllic": 11211141, - "Raryllic": 12111411, - "Zycryllic": 21114111, - "Mycryllic": 11141112, - "Laptyllic": 11411121, - "Pylyllic": 14111211, - "Pothyllic": 32111211, - "Phronyllic": 21112113, - "Stynyllic": 11121132, - "Rathyllic": 11211321, - "Aeryptyllic": 12113211, - "Zydyllic": 21132111, - "Katolyllic": 11321112, - "Rythyllic": 13211121, - "Locryllic": 31131111, - "Bylyllic": 11311113, - "Sogyllic": 13111131, - "Ionycryllic": 31111311, - "Koptyllic": 11113113, - "Epyryllic": 11131131, - "Soptyllic": 11311311, - "Aeolylyllic": 13113111, - "Aeracryllic": 31122111, - "Epygyllic": 11221113, - "Thonyllic": 12211131, - "Lanyllic": 22111311, - "Phrynyllic": 21113112, - "Lycryllic": 11131122, - "Ionyptyllic": 11311221, - "Epathyllic": 13112211, - "Dydyllic": 31121211, - "Thogyllic": 11212113, - "Rygyllic": 12121131, - "Bycryllic": 21211311, - "Zacryllic": 12113112, - "Panyllic": 21131121, - "Dyryllic": 11311212, - "Zathyllic": 13112121, - "Dagyllic": 31121112, - "Katalyllic": 11211123, - "Katoryllic": 12111231, - "Dodyllic": 21112311, - "Zogyllic": 11123112, - "Madyllic": 11231121, - "Dycryllic": 12311211, - "Aeologyllic": 23112111, - "Sydyllic": 31113111, - "Katogyllic": 11131113, - "Zygyllic": 11311131, - "Aeralyllic": 13111311, - "Bacryllic": 31112211, - "Aerygyllic": 11122113, - "Dathyllic": 11221131, - "Boptyllic": 12211311, - "Bagyllic": 22113111, - "Mathyllic": 21131112, - "Styptyllic": 11311122, - "Zolyllic": 13111221, - "Rocryllic": 22212111, - "Zyryllic": 22121112, - "Sagyllic": 21211122, - "Epinyllic": 12111222, - "Katagyllic": 21112221, - "Ragyllic": 11122212, - "Gothyllic": 11222121, - "Lythyllic": 12221211, - "Ionocryllic": 22211121, - "Gocryllic": 22111212, - "Epiryllic": 21112122, - "Aeradyllic": 11121222, - "Staptyllic": 11212221, - "Danyllic": 12122211, - "Goptyllic": 21222111, - "Epocryllic": 12221112, - "Ionoptyllic": 22121121, - "Aeoloryllic": 21211212, - "Thydyllic": 12112122, - "Gycryllic": 21121221, - "Lyryllic": 11212212, - "Mogyllic": 12122121, - "Katodyllic": 21221211, - "Moptyllic": 12212112, - "Dolyllic": 41211111, - "Moryllic": 12111114, - "Bydyllic": 21111141, - "Pocryllic": 11111412, - "Phracryllic": 11114121, - "Gyryllic": 11141211, - "Phrygyllic": 11412111, - "Dogyllic": 14121111, - "Thagyllic": 41121111, - "Thoptyllic": 11211114, - "Phraptyllic": 12111141, - "Gylyllic": 21111411, - "Phralyllic": 11114112, - "Dygyllic": 11141121, - "Ronyllic": 11411211, - "Epogyllic": 14112111, - "Aeoladyllic": 41111211, - "Kocryllic": 11112114, - "Lodyllic": 11121141, - "Bynyllic": 11211411, - "Kydyllic": 12114111, - "Bygyllic": 21141111, - "Phryptyllic": 11411112, - "Ionayllic": 14111121, - "Phroryllic": 41111121, - "Thyphyllic": 11111214, - "Poptyllic": 11112141, - "Mixonyllic": 11121411, - "Paptyllic": 11214111, - "Storyllic": 12141111, - "Phrycryllic": 21411111, - "Palyllic": 14111112, - "Phranyllic": 32211111, - "Stydyllic": 22111113, - "Zadyllic": 21111132, - "Zalyllic": 11111322, - "Zocryllic": 11113221, - "Katocryllic": 11132211, - "Aerathyllic": 11322111, - "Stoptyllic": 13221111, - "Lydyllic": 32121111, - "Radyllic": 21211113, - "Stagyllic": 12111132, - "Ionoryllic": 21111321, - "Phrodyllic": 11113212, - "Aeragyllic": 11132121, - "Banyllic": 11321211, - "Epothyllic": 13212111, - "Zoryllic": 32112111, - "Phrolyllic": 21121113, - "Kolyllic": 11211132, - "Thodyllic": 12111321, - "Socryllic": 21113211, - "Aeolyllic": 11132112, - "Zythyllic": 11321121, - "Aeoryllic": 13211211, - "Mixolydyllic": 32111112, - "Mixonyphyllic": 21111123, - "Aeolanyllic": 11111232, - "Thocryllic": 11112321, - "Kygyllic": 11123211, - "Ionagyllic": 11232111, - "Gogyllic": 12321111, - "Phradyllic": 23211111, - "Ioniptyllic": 31311111, - "Kycryllic": 13111113, - "Aeolaptyllic": 31111131, - "Rodyllic": 11111313, - "Ionathyllic": 11113131, - "Pythyllic": 11131311, - "Zonyllic": 11313111, - "Ryryllic": 13131111, - "Aeolothyllic": 31221111, - "Ionyryllic": 12211113, - "Rydyllic": 22111131, - "Gonyllic": 21111312, - "Rolyllic": 11113122, - "Katydyllic": 11131221, - "Zyptyllic": 11312211, - "Modyllic": 13122111, - "Maptyllic": 31212111, - "Aeraptyllic": 12121113, - "Katadyllic": 21211131, - "Magyllic": 12111312, - "Phrylyllic": 21113121, - "Epigyllic": 11131212, - "Molyllic": 11312121, - "Ponyllic": 13121211, - "Thyptyllic": 31211211, - "Ionogyllic": 12112113, - "Aeolaryllic": 21121131, - "Katygyllic": 11211312, - "Ganyllic": 12113121, - "Kyptyllic": 21131211, - "Salyllic": 11312112, - "Sanyllic": 13121121, - "Doptyllic": 31211121, - "Ionilyllic": 12111213, - "Manyllic": 21112131, - "Polyllic": 11121312, - "Stanyllic": 11213121, - "Mixotharyllic": 12131211, - "Eporyllic": 21312111, - "Aerynyllic": 13121112, - "Lonyllic": 31121121, - "Sathyllic": 11211213, - "Layllic": 12112131, - "Saryllic": 21121311, - "Thacryllic": 11213112, - "Aeolynyllic": 12131121, - "Thadyllic": 21311211, - "Lynyllic": 13112112, - "Aeolathyllic": 31112121, - "Aeolocryllic": 11121213, - "Phroptyllic": 11212131, - "Kodyllic": 12121311, - "Epaptyllic": 21213111, - "Ionoyllic": 12131112, - "Gyptyllic": 21311121, - "Aerythyllic": 13111212, - "Zagyllic": 31112112, - "Epacryllic": 11121123, - "Thorcryllic": 11211231, - "Loptyllic": 12112311, - "Katylyllic": 21123111, - "Malyllic": 11231112, - "Mydyllic": 12311121, - "Thycryllic": 23111211, - "Gythyllic": 31111221, - "Pyryllic": 11112213, - "Rycryllic": 11122131, - "Phrathyllic": 11221311, - "Badyllic": 12213111, - "Phrocryllic": 22131111, - "Staryllic": 21311112, - "Zothyllic": 13111122, - "Tharyllic": 31111212, - "Sylyllic": 11112123, - "Lothyllic": 11121231, - "Daryllic": 11212311, - "Monyllic": 12123111, - "Styryllic": 21231111, - "Aeolacryllic": 12311112, - "Raptyllic": 23111121, - "Kataryllic": 31111122, - "Aerocryllic": 11111223, - "Zanyllic": 11112231, - "Aeolonyllic": 11122311, - "Aeonyllic": 11223111, - "Kyryllic": 12231111, - "Sythyllic": 22311111, - "Katycryllic": 23111112, - "Stogyllic": 22121211, - "Ionidyllic": 21212112, - "Stonyllic": 12121122, - "Stalyllic": 21211221, - "Poryllic": 12112212, - "Mocryllic": 21122121, - "Aeolyryllic": 11221212, - "Baryllic": 12212121, - "Dalyllic": 22112121, - "Ionyphyllic": 21121212, - "Zaptyllic": 11212122, - "Garyllic": 12121221, - "Gathyllic": 21212211, - "Mixopyryllic": 12122112, - "Ionacryllic": 21221121, - "Stylyllic": 12211212, - "Stycryllic": 42111111, - "Ionothyllic": 21111114, - "Mythyllic": 11111142, - "Aerylyllic": 11111421, - "Bonyllic": 11114211, - "Tholyllic": 11142111, - "Katyryllic": 11421111, - "Sadyllic": 14211111, - "Stolyllic": 41111112, - "Logyllic": 11111124, - "Dacryllic": 11111241, - "Thynyllic": 11112411, - "Gydyllic": 11124111, - "Eparyllic": 11241111, - "Dynyllic": 12411111, - "Ionyllic": 24111111, - "Zaryllic": 33111111, - "Dythyllic": 31111113, - "Ionaryllic": 11111133, - "Laryllic": 11111331, - "Kataptyllic": 11113311, - "Sonyllic": 11133111, - "Pathyllic": 11331111, - "Loryllic": 13311111, - "Aeronyllic": 32111121, - "Pycryllic": 21111213, - "Mygyllic": 11112132, - "Lylyllic": 11121321, - "Daptyllic": 11213211, - "Ioninyllic": 12132111, - "Epaphyllic": 21321111, - "Lolyllic": 13211112, - "Stacryllic": 31211112, - "Doryllic": 12111123, - "Kadyllic": 21111231, - "Rynyllic": 11112312, - "Aerogyllic": 11123121, - "Rothyllic": 11231211, - "Kagyllic": 12312111, - "Stathyllic": 23121111, - "Thyryllic": 22221111, - "Gygyllic": 22211112, - "Sodyllic": 22111122, - "Goryllic": 21111222, - "Bothyllic": 11112222, - "Gynyllic": 11122221, - "Ionaptyllic": 11222211, - "Phryryllic": 12222111, - "Racryllic": 22211211, - "Epicryllic": 22112112, - "Stygyllic": 21121122, - "Syryllic": 11211222, - "Stythyllic": 12112221, - "Aerothyllic": 21122211, - "Mixoryllic": 11222112, - "Thanyllic": 12221121, - "Roryllic": 22112211, - "Epotyllic": 21122112, - "Epidyllic": 11221122, - "Kaptyllic": 12211221, - "MajorDimin.": 21212121, - "MinorDimin.": 12121212, - "Aerycrygic": 221112111, - "Gadygic": 211121112, - "Solygic": 111211122, - "Zylygic": 112111221, - "Garygic": 121112211, - "Sorygic": 211122111, - "Godygic": 111221112, - "Epithygic": 112211121, - "Ionoptygic": 122111211, - "Kalygic": 311211111, - "Ionodygic": 112111113, - "Bythygic": 121111131, - "Epygic": 211111311, - "Marygic": 111113112, - "Gaptygic": 111131121, - "Aeroptygic": 111311211, - "Mylygic": 113112111, - "Galygic": 131121111, - "Mixolydygic": 311121111, - "Ionycrygic": 111211113, - "Zoptygic": 112111131, - "Phrygygic": 121111311, - "Locrygic": 211113111, - "Gonygic": 111131112, - "Aeracrygic": 111311121, - "Aerathygic": 113111211, - "Dorygic": 131112111, - "Dycrygic": 311112111, - "Aeolygic": 111121113, - "Dydygic": 111211131, - "Tholygic": 112111311, - "Rynygic": 121113111, - "Bycrygic": 211131111, - "Zacrygic": 111311112, - "Panygic": 113111121, - "Dyrygic": 131111211, - "Loptygic": 311111211, - "Katylygic": 111112113, - "Phradygic": 111121131, - "Mixodygic": 111211311, - "Katalygic": 112113111, - "Katorygic": 121131111, - "Dogygic": 211311111, - "Zodygic": 113111112, - "Madygic": 131111121, - "Bagygic": 221211111, - "Mathygic": 212111112, - "Styptygic": 121111122, - "Zolygic": 211111221, - "Sydygic": 111112212, - "Katygic": 111122121, - "Zyphygic": 111221211, - "Aeralygic": 112212111, - "Ryptygic": 122121111, - "Apinygic": 221111121, - "Katagygic": 211111212, - "Radygic": 111112122, - "Gothygic": 111121221, - "Lythygic": 111212211, - "Bacrygic": 112122111, - "Aerygic": 121221111, - "Dathygic": 212211111, - "Boptygic": 122111112, - "Epyrygic": 212112111, - "Aeradygic": 121121112, - "Staptygic": 211211121, - "Danygic": 112111212, - "Goptygic": 121112121, - "Epocrygic": 211121211, - "Rocrygic": 111212112, - "Zyrygic": 112121121, - "Sadygic": 121211211, - "Aeolorygic": 212111211, - "Thydygic": 121112112, - "Gycrygic": 211121121, - "Lyrygic": 111211212, - "Modygic": 112112121, - "Katodygic": 121121211, - "Moptygic": 211212111, - "Ionocrygic": 112121112, - "Gocrygic": 121211121, - "Manygic": 411111111, - "Polygic": 111111114, - "Stanygic": 111111141, - "Thaptygic": 111111411, - "Eporygic": 111114111, - "Aerynygic": 111141111, - "Thyptygic": 111411111, - "Ionogygic": 114111111, - "Aeolarygic": 141111111, - "Sathygic": 321111111, - "Ladygic": 211111113, - "Sarygic": 111111132, - "Thacrygic": 111111321, - "Aeolynygic": 111113211, - "Thadygic": 111132111, - "Lynygic": 111321111, - "Doptygic": 113211111, - "Ionilygic": 132111111, - "Phrygic": 312111111, - "Aeranygic": 121111113, - "Dothygic": 211111131, - "Lydygic": 111111312, - "Stadygic": 111113121, - "Byptygic": 111131211, - "Stodygic": 111312111, - "Zynygic": 113121111, - "Lonygic": 131211111, - "Zothygic": 311111121, - "Aeolathygic": 111111213, - "Aeolocrygic": 111112131, - "Phroptygic": 111121311, - "Kodygic": 111213111, - "Eparygic": 112131111, - "Ionygic": 121311111, - "Gyptygic": 213111111, - "Aerythygic": 131111112, - "Aeolacrygic": 311111112, - "Raptygic": 111111123, - "Gythygic": 111111231, - "Pyrygic": 111112311, - "Rycrygic": 111123111, - "Phrathygic": 111231111, - "Badygic": 112311111, - "Phrocrygic": 123111111, - "Starygic": 231111111, - "Kyrygic": 222111111, - "Sythygic": 221111112, - "Katycrygic": 211111122, - "Tharygic": 111111222, - "Sylygic": 111112221, - "Lothygic": 111122211, - "Darygic": 111222111, - "Monygic": 112221111, - "Styrygic": 122211111, - "Porygic": 221121111, - "Mocrygic": 211211112, - "Aeolyrygic": 112111122, - "Barygic": 121111221, - "Katarygic": 211112211, - "Aerocrygic": 111122112, - "Zanygic": 111221121, - "Aeolonygic": 112211211, - "Aeolanygic": 122112111, - "Kaptygic": 221111211, - "Sacrygic": 211112112, - "Padygic": 111121122, - "Epilygic": 111211221, - "Kynygic": 112112211, - "Stophygic": 121122111, - "Ionidygic": 211221111, - "Stonygic": 112211112, - "Stalygic": 122111121, - "Koptygic": 212121111, - "Raphygic": 121211112, - "Zycrygic": 212111121, - "Mycrygic": 121111212, - "Laptygic": 211112121, - "Pylygic": 111121212, - "Rodygic": 111212121, - "Epolygic": 112121211, - "Epidygic": 121212111, - "Phronygic": 211211211, - "Stynygic": 112112112, - "Zydygic": 121121121, - "Aerycryllian": 2111211111, - "Gadyllian": 1112111112, - "Solyllian": 1121111121, - "Zyphyllian": 1211111211, - "Garyllian": 2111112111, - "Soryllian": 1111121112, - "Godyllian": 1111211121, - "Epityllian": 1112111211, - "Ionyllian": 1121112111, - "Aeoryllian": 1211121111, - "Katoryllian": 3111111111, - "Dodyllian": 1111111113, - "Zogyllian": 1111111131, - "Madyllian": 1111111311, - "Dycryllian": 1111113111, - "Aeogyllian": 1111131111, - "Dydyllian": 1111311111, - "Thogyllian": 1113111111, - "Rygyllian": 1131111111, - "Bathyllian": 1311111111, - "Sydyllian": 2211111111, - "Katogyllian": 2111111112, - "Mixodyllian": 1111111122, - "Aeradyllian": 1111111221, - "Ryptyllian": 1111112211, - "Loptyllian": 1111122111, - "Kataphyllian": 1111221111, - "Phradyllian": 1112211111, - "Dagyllian": 1122111111, - "Katyllian": 1221111111, - "Gothyllian": 2121111111, - "Lythyllian": 1211111112, - "Bacryllian": 2111111121, - "Aerygyllian": 1111111212, - "Dathyllian": 1111112121, - "Boptyllian": 1111121211, - "Bagyllian": 1111212111, - "Mathyllian": 1112121111, - "Styptyllian": 1121211111, - "Zolyllian": 1212111111, - "Staptyllian": 2112111111, - "Danyllian": 1121111112, - "Goptyllian": 1211111121, - "Epocryllian": 2111111211, - "Rocryllian": 1111112112, - "Zyryllian": 1111121121, - "Sagyllian": 1111211211, - "Epinyllian": 1112112111, - "Katagyllian": 1121121111, - "Ragyllian": 1211211111, - "Thydyllian": 2111121111, - "Epiryllian": 1111211112, - "Lyryllian": 1112111121, - "Mogyllian": 1121111211, - "Katodyllian": 1211112111, - "Aerycratic": 21111111111, - "Monatic": 11111111112, - "Solatic": 11111111121, - "Zylatic": 11111111211, - "Mixolatic": 11111112111, - "Soratic": 11111121111, - "Godatic": 11111211111, - "Eptatic": 11112111111, - "Ionatic": 11121111111, - "Aeolatic": 11211111111, - "Thydatic": 12111111111, - "Chromatic": 111111111111, -} - -note_to_interval = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} - -modifiers = { - "#": 1, - "b": -1, - "s": 1, -} - +from .defaults import SCALES, MODIFIERS, NOTE_TO_INTERVAL def note_to_midi(name: str) -> int: - """Parse note name to midi""" + """ Parse note name to midi + + Args: + name (str): Note name in scientific notation: [a-gA-G][#bs][1-9] + + Returns: + int: Midi note + """ items = re.match(r"^([a-gA-G])([#bs])?([1-9])?$", name) - if items == None: + if items is None: return 60 values = items.groups() octave = int(values[2]) if values[2] else 4 - modifier = modifiers[values[1]] if values[1] else 0 - interval = note_to_interval[values[0].capitalize()] + modifier = MODIFIERS[values[1]] if values[1] else 0 + 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""" +def get_scale(name: str) -> list[int]: + """ Get a scale from the global scale list + + Args: + name (str): Name of the scale as named in https://allthescales.org/ + + Returns: + list: List of intervals in the scale + """ scale = SCALES.get(name.lower().capitalize(), SCALES["Chromatic"]) return list(map(int, str(scale))) - +# pylint: disable=locally-disabled, too-many-arguments def note_from_pc( root: int | str, pitch_class: int, @@ -1531,7 +45,19 @@ def note_from_pc( octave: int = 0, modifier: int = 0, ) -> int: - """Resolve a pitch class into a note from a scale""" + """ Resolve a pitch class into a note from a scale + + Args: + root (int | str): Root of the scale in MIDI or scientific pitch notation + pitch_class (int): Pitch class to be resolved + intervals (str | list[int | float]): Name or Intervals for the scale + cents (bool, optional): Flag for interpreting intervals as cents. Defaults to False. + octave (int, optional): Default octave. Defaults to 0. + modifier (int, optional): Modifier for the pitch class (#=1, b=-1). Defaults to 0. + + Returns: + int: Resolved MIDI note + """ # Initialization root = note_to_midi(root) if isinstance(root, str) else root @@ -1540,13 +66,17 @@ def note_from_pc( scale_length = len(intervals) # Resolve pitch classes to the scale and calculate octave - if pitch_class>=scale_length or pitch_class<0: - octave += floor(pitch_class/scale_length) - pitch_class = scale_length-(abs(pitch_class)%scale_length) if pitch_class<0 else pitch_class%scale_length + if pitch_class >= scale_length or pitch_class < 0: + octave += floor(pitch_class / scale_length) + pitch_class = ( + scale_length - (abs(pitch_class) % scale_length) + if pitch_class < 0 + else pitch_class % scale_length + ) if pitch_class == scale_length: - pitch_class = 0 + pitch_class = 0 # Computing the result - note = root + sum(intervals[0 : pitch_class]) - + note = root + sum(intervals[0:pitch_class]) + return note + (octave * sum(intervals)) + modifier diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index 4862661..980bc6d 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -1,9 +1,9 @@ // Root for the rules ?root: sequence -> start - sequence: (pc | 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 | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)* // Pitch classes - pc: prefix* pitch + pitch_class: prefix* pitch prefix: (octave | duration_chars | escaped_decimal | escaped_octave) pitch: /-?[0-9TE]/ escaped_decimal: "<" decimal ">" @@ -11,7 +11,7 @@ octave: /[_^]+/ // Chords - chord: pc pc+ + chord: pitch_class pitch_class+ // Valid as integer ?number: SIGNED_NUMBER | random_integer | cyclic_number @@ -47,7 +47,7 @@ // Subdivision subdivision: "[" subitems "]" - subitems: (pc | WS | chord | cycle | subdivision)* + subitems: (pitch_class | WS | chord | cycle | subdivision)* // Control characters modifying future events oct_mod: octave WS From 04d84bcc47debde368b6a5fa0965fdc4c05667a6 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Tue, 7 Feb 2023 21:04:48 +0200 Subject: [PATCH 32/44] Added roman numerals --- tests/test_scale.py | 3 ++- ziffers/classes.py | 46 ++++++++++++++++++++++++++++---------------- ziffers/defaults.py | 10 ++++++++++ ziffers/mapper.py | 31 +++++++++++++++++++++++++---- ziffers/scale.py | 25 ++++++++++++++++++++---- ziffers/ziffers.lark | 5 ++++- 6 files changed, 93 insertions(+), 27 deletions(-) diff --git a/tests/test_scale.py b/tests/test_scale.py index 0458666..23f765b 100644 --- a/tests/test_scale.py +++ b/tests/test_scale.py @@ -1,6 +1,7 @@ """ Tests for the scale module """ -from ziffers import scale import pytest +from ziffers import scale + @pytest.mark.parametrize( diff --git a/ziffers/classes.py b/ziffers/classes.py index c72069a..13a293d 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -5,7 +5,6 @@ import operator import random from .defaults import DEFAULT_OPTIONS - @dataclass class Meta: """Abstract class for all Ziffers items""" @@ -30,6 +29,7 @@ class Item(Meta): text: str + @dataclass class Whitespace(Item): """Class for whitespace""" @@ -76,8 +76,8 @@ class Pitch(Event): """Class for pitch in time""" pitch_class: int = field(default=None) - duration: float = field(default=None) octave: int = field(default=None) + note: int = field(default=None) @dataclass @@ -101,6 +101,14 @@ class Chord(Event): 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 class Function(Event): """Class for functions""" @@ -129,9 +137,9 @@ class Sequence(Meta): next_item = self.values[self.local_index] self.local_index += 1 return next_item - else: - self.local_index = 0 - raise StopIteration + + self.local_index = 0 + raise StopIteration def update_values(self, new_values): """Update value attributes from dict""" @@ -165,7 +173,9 @@ class Ziffers(Sequence): options: dict = field(default_factory=DEFAULT_OPTIONS) loop_i: int = 0 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): super().__post_init__() @@ -193,7 +203,7 @@ class Ziffers(Sequence): return self.current 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: 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)) def set_defaults(self, options: dict): - """ Sets options for the parser + """Sets options for the parser Args: options (dict): Options as a dict @@ -213,19 +223,21 @@ class Ziffers(Sequence): # TODO: Refactor these 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)] 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)] def pairs(self) -> list[tuple]: - """ Return list of pitches and durations """ - return [(val.pitch_class, val.dur) for val in self.values if isinstance(val, Pitch)] + """Return list of pitches and durations""" + return [ + (val.pitch_class, val.dur) for val in self.values if isinstance(val, Pitch) + ] 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)] @@ -258,7 +270,7 @@ class RandomInteger(Item): self.max = new_max def value(self): - """ Evaluate the random value for the generator """ + """Evaluate the random value for the generator""" 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)] def value(self): - """ Get the value for the current cycle """ + """Get the value for the current cycle""" return self.values[self.cycle] def next_cycle(self, cycle: int): - """ Evaluate next cycle """ + """Evaluate next cycle""" self.cycle = self.cycle + 1 @@ -332,7 +344,7 @@ class ListOperation(Sequence): """Class for list operations""" def run(self): - """ Run operations """ + """Run operations""" pass diff --git a/ziffers/defaults.py b/ziffers/defaults.py index c0727e5..e2f02ba 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -51,6 +51,16 @@ MODIFIERS = { "s": 1, } +ROMANS = { + 'i': 1, + 'v': 5, + 'x': 10, + 'l': 50, + 'c': 100, + 'd': 500, + 'm': 1000 + } + # pylint: disable=locally-disabled, too-many-lines SCALES = { diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 364daa0..160c245 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -1,4 +1,5 @@ """ Lark transformer for mapping Lark tokens to Ziffers objects """ +from typing import Optional from lark import Transformer from .classes import ( Ziffers, @@ -10,6 +11,7 @@ from .classes import ( RandomPitch, RandomPercent, Chord, + RomanNumeral, Sequence, ListSequence, RepeatedListSequence, @@ -28,13 +30,18 @@ from .classes import ( ) from .common import flatten, sum_dict 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 class ZiffersTransformer(Transformer): """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""" seq = Sequence(values=items[0]) return Ziffers(values=seq, options={}) @@ -43,17 +50,17 @@ class ZiffersTransformer(Transformer): """Flatten sequence""" return flatten(items) - def random_integer(self, item): + def random_integer(self, item) -> RandomInteger: """Parses random integer syntax""" val = item[0][1:-1].split(",") return RandomInteger(min=val[0], max=val[1], text=item[0].value) - def range(self, item): + def range(self, item) -> Range: """Parses range syntax""" val = item[0].split("..") return Range(start=val[0], end=val[1], text=item[0]) - def cycle(self, items): + def cycle(self, items) -> Cyclic: """Parses cycle""" values = items[0] return Cyclic(values=values) @@ -102,6 +109,22 @@ class ZiffersTransformer(Transformer): """Parses chord""" 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): """Parses duration change""" durs = items[0] diff --git a/ziffers/scale.py b/ziffers/scale.py index 6541ae8..2489807 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -3,10 +3,11 @@ # pylint: disable=locally-disabled, no-name-in-module import re 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: - """ Parse note name to midi + """Parse note name to midi Args: 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]: - """ Get a scale from the global scale list + """Get a scale from the global scale list Args: 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"]) return list(map(int, str(scale))) + # pylint: disable=locally-disabled, too-many-arguments def note_from_pc( root: int | str, @@ -45,7 +47,7 @@ def note_from_pc( octave: int = 0, modifier: int = 0, ) -> int: - """ Resolve a pitch class into a note from a scale + """Resolve a pitch class into a note from a scale Args: 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]) 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 diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index 980bc6d..c8310d1 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -1,6 +1,6 @@ // Root for the rules ?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_class: prefix* pitch @@ -12,6 +12,9 @@ // Chords 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 ?number: SIGNED_NUMBER | random_integer | cyclic_number From 4bcdb2568a52909c3b266e69de78dd6c99ce3a3b Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Tue, 7 Feb 2023 22:26:20 +0200 Subject: [PATCH 33/44] Added note parsing --- ziffers/classes.py | 12 ++++++++++++ ziffers/mapper.py | 2 +- ziffers/parser.py | 42 +++++++++++++++++++++--------------------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index 13a293d..8a18810 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -4,6 +4,7 @@ import itertools import operator import random from .defaults import DEFAULT_OPTIONS +from .scale import note_from_pc @dataclass class Meta: @@ -79,6 +80,9 @@ class Pitch(Event): octave: int = field(default=None) note: int = field(default=None) + def set_note(self,note: int): + self.note = note + @dataclass class RandomPitch(Event): @@ -198,7 +202,15 @@ class Ziffers(Sequence): self.current = next(self.iterator) # Skip item + # Update collected options & default options self.current.update_new(self.options) + + # Resolve note from scale + if set(("key","scale")) <= self.options.keys(): + if isinstance(self.current,(Pitch,RandomPitch)): + note = note_from_pc(self.options["key"],self.current.pitch_class,self.options["scale"]) + self.current.set_note(note) + self.loop_i += 1 return self.current diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 160c245..f0c7db2 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -30,7 +30,7 @@ from .classes import ( ) from .common import flatten, sum_dict from .defaults import DEFAULT_DURS -from .scale import note_from_pc, parse_roman +from .scale import parse_roman # pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name diff --git a/ziffers/parser.py b/ziffers/parser.py index c162111..683d5e0 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -29,7 +29,7 @@ def parse_expression(expr: str) -> Ziffers: return ziffers_parser.parse(expr) -def zparse(expr: str, opts: dict = None) -> Ziffers: +def zparse(expr: str, **opts) -> Ziffers: """Parses ziffers expression with options Args: @@ -48,51 +48,51 @@ def zparse(expr: str, opts: dict = None) -> Ziffers: # pylint: disable=invalid-name -def z0(expr: str, opts: dict = None) -> Ziffers: +def z0(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) -def z1(expr: str, opts: dict = None) -> Ziffers: +def z1(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) -def z2(expr: str, opts: dict = None) -> Ziffers: +def z2(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) -def z3(expr: str, opts: dict = None) -> Ziffers: +def z3(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) -def z4(expr: str, opts: dict = None) -> Ziffers: +def z4(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) -def z5(expr: str, opts: dict = None) -> Ziffers: +def z5(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) -def z6(expr: str, opts: dict = None) -> Ziffers: +def z6(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) -def z7(expr: str, opts: dict = None) -> Ziffers: +def z7(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) -def z8(expr: str, opts: dict = None) -> Ziffers: +def z8(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) -def z9(expr: str, opts: dict = None) -> Ziffers: +def z9(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" - return zparse(expr, opts) + return zparse(expr, **opts) From 8d49e5d5c21ab508bbda5ba6b41ba8a0106aa121 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 8 Feb 2023 20:43:57 +0200 Subject: [PATCH 34/44] Added chord names Chord names, parsing notes from roman numerals, method for resolving pitch classes from midi notes (not in use yet). --- ziffers/__init__.py | 1 + ziffers/classes.py | 24 +++++- ziffers/defaults.py | 134 ++++++++++++++++++++++++++++---- ziffers/mapper.py | 7 +- ziffers/scale.py | 177 +++++++++++++++++++++++++++++++++++++++++-- ziffers/ziffers.lark | 2 +- 6 files changed, 318 insertions(+), 27 deletions(-) diff --git a/ziffers/__init__.py b/ziffers/__init__.py index ebecbbc..313cce0 100644 --- a/ziffers/__init__.py +++ b/ziffers/__init__.py @@ -3,3 +3,4 @@ from .mapper import * from .classes import * from .common import * from .defaults import * +from .scale import * diff --git a/ziffers/classes.py b/ziffers/classes.py index 8a18810..611a47b 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -6,6 +6,7 @@ import random from .defaults import DEFAULT_OPTIONS from .scale import note_from_pc + @dataclass class Meta: """Abstract class for all Ziffers items""" @@ -80,8 +81,9 @@ class Pitch(Event): octave: int = field(default=None) note: int = field(default=None) - def set_note(self,note: int): + def set_note(self, note: int): self.note = note + return note @dataclass @@ -103,7 +105,11 @@ class Chord(Event): """Class for chords""" pitch_classes: list[Pitch] = field(default=None) + notes: list[int] = field(default=None) + def set_notes(self, notes: list[int]): + """Set notes to the class""" + self.notes = notes @dataclass class RomanNumeral(Event): @@ -111,6 +117,7 @@ class RomanNumeral(Event): value: str = field(default=None) chord_type: str = field(default=None) + notes: list[int] = field(default_factory=[]) @dataclass @@ -206,10 +213,16 @@ class Ziffers(Sequence): self.current.update_new(self.options) # Resolve note from scale - if set(("key","scale")) <= self.options.keys(): - if isinstance(self.current,(Pitch,RandomPitch)): - note = note_from_pc(self.options["key"],self.current.pitch_class,self.options["scale"]) + if set(("key", "scale")) <= self.options.keys(): + key = self.options["key"] + scale = self.options["scale"] + if isinstance(self.current, (Pitch, RandomPitch)): + note = note_from_pc(key,self.current.pitch_class,scale) self.current.set_note(note) + elif isinstance(self.current,Chord): + pcs = self.current.pitch_classes + notes = [pc.set_note(note_from_pc(key, pc.pitch_class, scale)) for pc in pcs] + self.current.set_notes(notes) self.loop_i += 1 return self.current @@ -225,6 +238,9 @@ class Ziffers(Sequence): """ return list(itertools.islice(itertools.cycle(self), num)) + def loop(self) -> iter: + return itertools.cycle(self.iterator) + def set_defaults(self, options: dict): """Sets options for the parser diff --git a/ziffers/defaults.py b/ziffers/defaults.py index e2f02ba..e4062db 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -38,12 +38,41 @@ DEFAULT_DURS = { "z": 0.0, # 0 } -DEFAULT_OPTIONS = { - "octave": 0, - "duration": 0.25 +DEFAULT_OCTAVE = 4 + +DEFAULT_OPTIONS = {"octave": 0, "duration": 0.25} + +NOTES_TO_INTERVALS = { + 'C': 0, + 'Cs': 1, + 'D': 2, + 'Eb': 3, + 'E': 4, + 'F': 5, + 'Fs': 6, + 'G': 7, + 'Ab': 8, + 'A': 9, + 'Bb': 10, + 'B': 11 + } + +INTERVALS_TO_NOTES = { + 0: 'C', + 1: 'Cs', + 2: 'D', + 3: 'Eb', + 4: 'E', + 5: 'F', + 6: 'Fs', + 7: 'G', + 8: 'Ab', + 9: 'A', + 10: 'Bb', + 11: 'B' } -NOTE_TO_INTERVAL = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} +CIRCLE_OF_FIFTHS = ['Gb', 'Cs', 'Ab', 'Eb', 'Bb', 'F', 'C', 'G', 'D', 'A', 'E', 'B', 'Fs'] MODIFIERS = { "#": 1, @@ -51,15 +80,7 @@ MODIFIERS = { "s": 1, } -ROMANS = { - 'i': 1, - 'v': 5, - 'x': 10, - 'l': 50, - 'c': 100, - 'd': 500, - 'm': 1000 - } +ROMANS = {"i": 1, "v": 5, "x": 10, "l": 50, "c": 100, "d": 500, "m": 1000} # pylint: disable=locally-disabled, too-many-lines @@ -1555,3 +1576,90 @@ SCALES = { "Thydatic": 12111111111, "Chromatic": 111111111111, } + +def __build_chords(): + major = [0, 4, 7] + minor = [0, 3, 7] + major7 = [0, 4, 7, 11] + dom7 = [0, 4, 7, 10] + minor7 = [0, 3, 7, 10] + aug = [0, 4, 8] + dim = [0, 3, 6] + dim7 = [0, 3, 6, 9] + halfdim = [0, 3, 6, 10] + all_chords = { + "1": [0], + "5": [0, 7], + "+5": [0, 4, 8], + "m+5": [0, 3, 8], + "sus2": [0, 2, 7], + "sus4": [0, 5, 7], + "6": [0, 4, 7, 9], + "m6": [0, 3, 7, 9], + "7sus2": [0, 2, 7, 10], + "7sus4": [0, 5, 7, 10], + "7-5": [0, 4, 6, 10], + "7+5": [0, 4, 8, 10], + "m7+5": [0, 3, 8, 10], + "9": [0, 4, 7, 10, 14], + "m9": [0, 3, 7, 10, 14], + "m7+9": [0, 3, 7, 10, 14], + "maj9": [0, 4, 7, 11, 14], + "9sus4": [0, 5, 7, 10, 14], + "6*9": [0, 4, 7, 9, 14], + "m6*9": [0, 3, 7, 9, 14], + "7-9": [0, 4, 7, 10, 13], + "m7-9": [0, 3, 7, 10, 13], + "7-10": [0, 4, 7, 10, 15], + "7-11": [0, 4, 7, 10, 16], + "7-13": [0, 4, 7, 10, 20], + "9+5": [0, 10, 13], + "m9+5": [0, 10, 14], + "7+5-9": [0, 4, 8, 10, 13], + "m7+5-9": [0, 3, 8, 10, 13], + "11": [0, 4, 7, 10, 14, 17], + "m11": [0, 3, 7, 10, 14, 17], + "maj11": [0, 4, 7, 11, 14, 17], + "11+": [0, 4, 7, 10, 14, 18], + "m11+": [0, 3, 7, 10, 14, 18], + "13": [0, 4, 7, 10, 14, 17, 21], + "m13": [0, 3, 7, 10, 14, 17, 21], + "add2": [0, 2, 4, 7], + "add4": [0, 4, 5, 7], + "add9": [0, 4, 7, 14], + "add11": [0, 4, 7, 17], + "add13": [0, 4, 7, 21], + "madd2": [0, 2, 3, 7], + "madd4": [0, 3, 5, 7], + "madd9": [0, 3, 7, 14], + "madd11": [0, 3, 7, 17], + "madd13": [0, 3, 7, 21], + "major": major, + "maj": major, + "M": major, + "minor": minor, + "min": minor, + "m": minor, + "major7": major7, + "dom7": dom7, + "7": dom7, + "M7": major7, + "minor7": minor7, + "m7": minor7, + "augmented": aug, + "a": aug, + "aug": aug, + "diminished": dim, + "dim": dim, + "i": dim, + "diminished7": dim7, + "dim7": dim7, + "i7": dim7, + "halfdim": halfdim, + "m7b5": halfdim, + "m7-5": halfdim, + } + all_chords_names = list(all_chords.keys()) + return (all_chords, all_chords_names) + +CHORDS, CHORD_NAMES = __build_chords() diff --git a/ziffers/mapper.py b/ziffers/mapper.py index f0c7db2..312ee7b 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -30,7 +30,7 @@ from .classes import ( ) from .common import flatten, sum_dict from .defaults import DEFAULT_DURS -from .scale import parse_roman +from .scale import parse_roman, chord_from_roman_numeral # pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name @@ -114,8 +114,9 @@ class ZiffersTransformer(Transformer): 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) + notes = chord_from_roman_numeral(numeral,name) + return RomanNumeral(text=numeral, value=parse_roman(numeral), chord_type=name, notes=notes) + return RomanNumeral(value=parse_roman(numeral), text=numeral, notes=chord_from_roman_numeral(numeral)) def chord_name(self,item): """Return name for chord""" diff --git a/ziffers/scale.py b/ziffers/scale.py index 2489807..ef43b8f 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -3,10 +3,49 @@ # pylint: disable=locally-disabled, no-name-in-module import re from math import floor -from .defaults import SCALES, MODIFIERS, NOTE_TO_INTERVAL, ROMANS +from .defaults import ( + DEFAULT_OCTAVE, + SCALES, + MODIFIERS, + NOTES_TO_INTERVALS, + INTERVALS_TO_NOTES, + ROMANS, + CIRCLE_OF_FIFTHS, + CHORDS, +) -def note_to_midi(name: str) -> int: +def midi_to_note_name(midi: int) -> str: + """Creates note name from midi number + + Args: + midi (int): Mii number + + Returns: + str: Note name + """ + return INTERVALS_TO_NOTES[midi % 12] + + +def note_name_to_interval(name: str) -> int: + """Parse note name to interval + + Args: + name (str): Note name as: [a-gA-G][#bs] + + Returns: + int: Interval of the note name [-1 - 11] + """ + items = re.match(r"^([a-gA-G])([#bs])?$", name) + if items is None: + return 0 + values = items.groups() + modifier = MODIFIERS[values[1]] if values[1] else 0 + interval = NOTES_TO_INTERVALS[values[0].capitalize()] + return interval + modifier + + +def note_name_to_midi(name: str) -> int: """Parse note name to midi Args: @@ -21,7 +60,7 @@ def note_to_midi(name: str) -> int: values = items.groups() octave = int(values[2]) if values[2] else 4 modifier = MODIFIERS[values[1]] if values[1] else 0 - interval = NOTE_TO_INTERVAL[values[0].capitalize()] + interval = NOTES_TO_INTERVALS[values[0].capitalize()] return 12 + octave * 12 + interval + modifier @@ -62,7 +101,7 @@ def note_from_pc( """ # Initialization - root = note_to_midi(root) if isinstance(root, str) else root + root = note_name_to_midi(root) if isinstance(root, str) else root intervals = get_scale(intervals) if isinstance(intervals, str) else intervals intervals = list(map(lambda x: x / 100), intervals) if cents else intervals scale_length = len(intervals) @@ -84,8 +123,15 @@ def note_from_pc( return note + (octave * sum(intervals)) + modifier -def parse_roman(numeral: str): - """Parse roman numeral from string""" +def parse_roman(numeral: str) -> int: + """Parse roman numeral from string + + Args: + numeral (str): Roman numeral as string + + Returns: + int: Integer parsed from roman numeral + """ values = [ROMANS[val] for val in numeral] result = 0 i = 0 @@ -97,3 +143,122 @@ def parse_roman(numeral: str): result += values[i] i += 1 return result + + +def accidentals_from_note_name(name: str) -> int: + """Generates number of accidentals from name of the note. + + Args: + name (str): Name of the note + + Returns: + int: Integer representing number of flats or sharps: -7 flat to 7 sharp. + """ + idx = CIRCLE_OF_FIFTHS.index(name.upper()) + return idx - 6 + + +def accidentals_from_midi_note(note: int) -> int: + """Generates number of accidentals from name of the note. + + Args: + note (int): Note as midi number + + Returns: + int: Integer representing number of flats or sharps: -7 flat to 7 sharp. + """ + name = midi_to_note_name(note) + return accidentals_from_note_name(name) + + +def midi_to_tpc(note: int, key: str | int): + """Return Tonal Pitch Class value for the note + + Args: + note (int): MIDI note + key (str | int): Key as a string (A-G) or a MIDI note. + + Returns: + _type_: Tonal Pitch Class value for the note + """ + if isinstance(key, str): + acc = accidentals_from_note_name(key) + else: + acc = accidentals_from_midi_note(key) + return (note * 7 + 26 - (11 + acc)) % 12 + (11 + acc) + + +def midi_to_pitch_class(note: int) -> int: + """Return pitch class from midi + + Args: + note (int): Note in midi + + Returns: + int: Returns note % 12 + """ + return note % 12 + + +def midi_to_octave(note: int) -> int: + """Return octave for the midi note + + Args: + note (int): Note in midi + + Returns: + int: Returns default octave in Ziffers where C4 is in octave 0 + """ + return 0 if note <= 0 else floor(note / 12) + + +def midi_to_pc(note: int, key: str | int, scale: str) -> tuple: + """Return pitch class and octave from given midi note, key and scale + + Args: + note (int): Note as MIDI number + key (str | int): Used key + scale (str): Used scale + + Returns: + tuple: Returns tuple containing (pitch class as string, pitch class, octave, optional modifier) + """ + sharps = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"] + flats = ["0", "b1", "1", "b2", "2", "3", "b4", "4", "b5", "5", "b6", "6"] + tpc = midi_to_tpc(note, key) + pitch_class = midi_to_pitch_class(note) + octave = midi_to_octave(note) - 5 + if scale.upper() == "CHROMATIC": + return (str(pitch_class), pitch_class, octave) + if tpc >= 6 and tpc <= 12 and len(flats[pitch_class]) == 2: + npc = flats[pitch_class] + elif tpc >= 20 and tpc <= 26 and len(sharps[pitch_class]) == 2: + npc = sharps[pitch_class] + else: + npc = sharps[pitch_class] + + if len(npc) > 1: + return (npc, int(npc[1]), octave, 1 if (npc[0] == "#") else -1) + + return (npc, int(npc), octave) + + +def chord_from_roman_numeral(roman: str, name: str = "major", num_octaves: int = 1) -> list[int]: + """Generates chord from given roman numeral and chord name + + Args: + roman (str): Roman numeral + name (str, optional): Chord name. Defaults to "major". + num_octaves (int, optional): Number of octaves for the chord. Defaults to 1. + + Returns: + list[int]: _description_ + """ + root = parse_roman(roman) - 1 + tonic = (DEFAULT_OCTAVE * 12) + root + 12 + intervals = CHORDS.get(name,CHORDS["major"]) + notes = [] + for cur_oct in range(num_octaves): + for iterval in intervals: + notes.append(tonic + iterval + (cur_oct * 12)) + return notes diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index c8310d1..a2de728 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -12,7 +12,7 @@ // Chords chord: pitch_class pitch_class+ - named_roman: roman_number (("^" chord_name) | ("+" number))? + named_roman: roman_number (("^" chord_name))? // TODO: Add | ("+" number) chord_name: /[a-zA-Z0-9]+/ ?roman_number: /iv|v|v?i{1,3}/ From bb928f7c6408ed68d2414c58ec418169a0f3868c Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 8 Feb 2023 21:47:44 +0200 Subject: [PATCH 35/44] Added pitch resolving to roman numeral chords --- ziffers/classes.py | 19 +++++++++++++++++-- ziffers/mapper.py | 5 +++-- ziffers/scale.py | 38 +++++++++++++++++--------------------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index 611a47b..d5852a5 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -4,7 +4,7 @@ import itertools import operator import random from .defaults import DEFAULT_OPTIONS -from .scale import note_from_pc +from .scale import note_from_pc, midi_to_pitch_class @dataclass @@ -79,6 +79,7 @@ class Pitch(Event): pitch_class: int = field(default=None) octave: int = field(default=None) + modifier: int = field(default=None) note: int = field(default=None) def set_note(self, note: int): @@ -118,6 +119,16 @@ class RomanNumeral(Event): value: str = field(default=None) chord_type: str = field(default=None) notes: list[int] = field(default_factory=[]) + pitch_classes: list = None + + def set_notes(self, chord_notes: list[int]): + self.notes = chord_notes + + def set_pitch_classes(self, pitches: list[tuple]): + if self.pitch_classes == None: + self.pitch_classes = [] + for pitch in pitches: + self.pitch_classes.append(Pitch(**pitch)) @dataclass @@ -212,7 +223,7 @@ class Ziffers(Sequence): # Update collected options & default options self.current.update_new(self.options) - # Resolve note from scale + # Resolve note(s) from scale if set(("key", "scale")) <= self.options.keys(): key = self.options["key"] scale = self.options["scale"] @@ -223,6 +234,10 @@ class Ziffers(Sequence): pcs = self.current.pitch_classes notes = [pc.set_note(note_from_pc(key, pc.pitch_class, scale)) for pc in pcs] self.current.set_notes(notes) + elif isinstance(self.current,RomanNumeral): + pitch_classes = [midi_to_pitch_class(note, key, scale) for note in self.current.notes] + self.current.set_pitch_classes(pitch_classes) + self.loop_i += 1 return self.current diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 312ee7b..cf9da26 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -114,8 +114,9 @@ class ZiffersTransformer(Transformer): numeral = items[0].value if len(items)>1: name = items[1] - notes = chord_from_roman_numeral(numeral,name) - return RomanNumeral(text=numeral, value=parse_roman(numeral), chord_type=name, notes=notes) + chord_notes = chord_from_roman_numeral(numeral,name) + parsed_number = parse_roman(numeral) + return RomanNumeral(text=numeral, value=parsed_number, chord_type=name, notes=chord_notes) return RomanNumeral(value=parse_roman(numeral), text=numeral, notes=chord_from_roman_numeral(numeral)) def chord_name(self,item): diff --git a/ziffers/scale.py b/ziffers/scale.py index ef43b8f..bc24428 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -188,18 +188,6 @@ def midi_to_tpc(note: int, key: str | int): return (note * 7 + 26 - (11 + acc)) % 12 + (11 + acc) -def midi_to_pitch_class(note: int) -> int: - """Return pitch class from midi - - Args: - note (int): Note in midi - - Returns: - int: Returns note % 12 - """ - return note % 12 - - def midi_to_octave(note: int) -> int: """Return octave for the midi note @@ -212,7 +200,7 @@ def midi_to_octave(note: int) -> int: return 0 if note <= 0 else floor(note / 12) -def midi_to_pc(note: int, key: str | int, scale: str) -> tuple: +def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict: """Return pitch class and octave from given midi note, key and scale Args: @@ -223,13 +211,14 @@ def midi_to_pc(note: int, key: str | int, scale: str) -> tuple: Returns: tuple: Returns tuple containing (pitch class as string, pitch class, octave, optional modifier) """ - sharps = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"] - flats = ["0", "b1", "1", "b2", "2", "3", "b4", "4", "b5", "5", "b6", "6"] - tpc = midi_to_tpc(note, key) - pitch_class = midi_to_pitch_class(note) + pitch_class = note % 12 octave = midi_to_octave(note) - 5 if scale.upper() == "CHROMATIC": return (str(pitch_class), pitch_class, octave) + + sharps = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"] + flats = ["0", "b1", "1", "b2", "2", "3", "b4", "4", "b5", "5", "b6", "6"] + tpc = midi_to_tpc(note, key) if tpc >= 6 and tpc <= 12 and len(flats[pitch_class]) == 2: npc = flats[pitch_class] elif tpc >= 20 and tpc <= 26 and len(sharps[pitch_class]) == 2: @@ -238,12 +227,19 @@ def midi_to_pc(note: int, key: str | int, scale: str) -> tuple: npc = sharps[pitch_class] if len(npc) > 1: - return (npc, int(npc[1]), octave, 1 if (npc[0] == "#") else -1) + return { + "text": npc, + "pitch_class": int(npc[1]), + "octave": octave, + "modifier": 1 if (npc[0] == "#") else -1, + } - return (npc, int(npc), octave) + return {"text": npc, "pitch_class": int(npc), "octave": octave} -def chord_from_roman_numeral(roman: str, name: str = "major", num_octaves: int = 1) -> list[int]: +def chord_from_roman_numeral( + roman: str, name: str = "major", num_octaves: int = 1 +) -> list[int]: """Generates chord from given roman numeral and chord name Args: @@ -256,7 +252,7 @@ def chord_from_roman_numeral(roman: str, name: str = "major", num_octaves: int = """ root = parse_roman(roman) - 1 tonic = (DEFAULT_OCTAVE * 12) + root + 12 - intervals = CHORDS.get(name,CHORDS["major"]) + intervals = CHORDS.get(name, CHORDS["major"]) notes = [] for cur_oct in range(num_octaves): for iterval in intervals: From f51b230099d65782f204327d82249dd7c04bc8b5 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 8 Feb 2023 22:34:30 +0200 Subject: [PATCH 36/44] Added modifier Added modifier and refactored cycles --- ziffers/classes.py | 18 +++++++----------- ziffers/mapper.py | 7 ++++++- ziffers/ziffers.lark | 3 ++- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index d5852a5..3ba4d4f 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -79,7 +79,7 @@ class Pitch(Event): pitch_class: int = field(default=None) octave: int = field(default=None) - modifier: int = field(default=None) + modifier: int = field(default=0) note: int = field(default=None) def set_note(self, note: int): @@ -228,7 +228,7 @@ class Ziffers(Sequence): key = self.options["key"] scale = self.options["scale"] if isinstance(self.current, (Pitch, RandomPitch)): - note = note_from_pc(key,self.current.pitch_class,scale) + note = note_from_pc(root=key,pitch_class=self.current.pitch_class,intervals=scale,modifier=self.current.modifier) self.current.set_note(note) elif isinstance(self.current,Chord): pcs = self.current.pitch_classes @@ -341,18 +341,14 @@ class Cyclic(Sequence): 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 isinstance(val, Whitespace)] + def __next__(self): + yield self.values[self.cycle%len(self.cycle)] + self.cycle+=1 + raise StopIteration def value(self): """Get the value for the current cycle""" - return self.values[self.cycle] - - def next_cycle(self, cycle: int): - """Evaluate next cycle""" - self.cycle = self.cycle + 1 + return self.values[self.cycle%len(self.cycle)] @dataclass diff --git a/ziffers/mapper.py b/ziffers/mapper.py index cf9da26..40953d8 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -101,10 +101,15 @@ class ZiffersTransformer(Transformer): return {"octave": int(value), "text": items[0].value} def octave(self, items): - """Return octave info""" + """Return octaves ^ and _""" value = sum(1 if char == "^" else -1 for char in items[0].value) return {"octave": value, "text": items[0].value} + def modifier(self, items): + """Return modifiers # and b""" + value = 1 if items[0].value == "#" else -1 + return {"modifier": value} + def chord(self, items): """Parses chord""" return Chord(pitch_classes=items, text="".join([val.text for val in items])) diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index a2de728..e2c0f28 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -4,11 +4,12 @@ // Pitch classes pitch_class: prefix* pitch - prefix: (octave | duration_chars | escaped_decimal | escaped_octave) + prefix: (octave | duration_chars | escaped_decimal | escaped_octave | modifier) pitch: /-?[0-9TE]/ escaped_decimal: "<" decimal ">" escaped_octave: /<-?[0-9]>/ octave: /[_^]+/ + modifier: /[#b]/ // Chords chord: pitch_class pitch_class+ From 691a2e400d4ac37a5c874a70c9564b488bfab44e Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Wed, 8 Feb 2023 22:54:14 +0100 Subject: [PATCH 37/44] better behaviour --- : | 20 ++++++++++++++++++++ repl.py | 4 ++-- test.py | 4 ++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 : create mode 100644 test.py diff --git a/: b/: new file mode 100644 index 0000000..0235826 --- /dev/null +++ b/: @@ -0,0 +1,20 @@ +""" Simple REPL for testing Ziffers notation """ +# pylint: disable=locally-disabled, redefined-builtin, broad-except + +from rich import print +from ziffers import parse_expression, zparse +import readline + +EXIT_CONDITION = ("exit", "quit", "") + +if __name__ == "__main__": + print("[purple]== ZIFFERS REPL ==[/purple]") + while True: + expr = input("> ") + if expr not in EXIT_CONDITION: + try: + print(parse_expression(expr)) + except Exception as e: + print(f"[red]Failed:[/red] {e}") + else: + exit() diff --git a/repl.py b/repl.py index b977f67..1d51a2d 100644 --- a/repl.py +++ b/repl.py @@ -3,6 +3,7 @@ from rich import print from ziffers import parse_expression +import readline EXIT_CONDITION = ("exit", "quit", "") @@ -12,8 +13,7 @@ if __name__ == "__main__": expr = input("> ") if expr not in EXIT_CONDITION: try: - result = parse_expression(expr) - print(result) + print(parse_expression(expr)) except Exception as e: print(f"[red]Failed:[/red] {e}") else: diff --git a/test.py b/test.py new file mode 100644 index 0000000..28f0a4f --- /dev/null +++ b/test.py @@ -0,0 +1,4 @@ +from ziffers import * +# a = z1("q e 1 ^ ^ 2 _ 3 4 <3> 3") +a = z1("1 2") +print(a.take(3)) From 0d9b1e54921396a2e8ba95ae870d49ab39781b8e Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Wed, 8 Feb 2023 22:55:18 +0100 Subject: [PATCH 38/44] fix --- : | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 : diff --git a/: b/: deleted file mode 100644 index 0235826..0000000 --- a/: +++ /dev/null @@ -1,20 +0,0 @@ -""" Simple REPL for testing Ziffers notation """ -# pylint: disable=locally-disabled, redefined-builtin, broad-except - -from rich import print -from ziffers import parse_expression, zparse -import readline - -EXIT_CONDITION = ("exit", "quit", "") - -if __name__ == "__main__": - print("[purple]== ZIFFERS REPL ==[/purple]") - while True: - expr = input("> ") - if expr not in EXIT_CONDITION: - try: - print(parse_expression(expr)) - except Exception as e: - print(f"[red]Failed:[/red] {e}") - else: - exit() From 55b6940f35f2862adfd2e35bd44653e7c7e8fad5 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Thu, 9 Feb 2023 00:16:26 +0200 Subject: [PATCH 39/44] Added parentheses support to eval and some formatting --- ziffers/classes.py | 27 ++++++++++++++++++--------- ziffers/mapper.py | 30 ++++++++++++++++++++---------- ziffers/ziffers.lark | 11 ++++++----- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index 3ba4d4f..1a241a4 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -112,6 +112,7 @@ class Chord(Event): """Set notes to the class""" self.notes = notes + @dataclass class RomanNumeral(Event): """Class for roman numbers""" @@ -228,17 +229,25 @@ class Ziffers(Sequence): key = self.options["key"] scale = self.options["scale"] if isinstance(self.current, (Pitch, RandomPitch)): - note = note_from_pc(root=key,pitch_class=self.current.pitch_class,intervals=scale,modifier=self.current.modifier) + note = note_from_pc( + root=key, + pitch_class=self.current.pitch_class, + intervals=scale, + modifier=self.current.modifier, + ) self.current.set_note(note) - elif isinstance(self.current,Chord): + elif isinstance(self.current, Chord): pcs = self.current.pitch_classes - notes = [pc.set_note(note_from_pc(key, pc.pitch_class, scale)) for pc in pcs] + notes = [ + pc.set_note(note_from_pc(key, pc.pitch_class, scale)) for pc in pcs + ] self.current.set_notes(notes) - elif isinstance(self.current,RomanNumeral): - pitch_classes = [midi_to_pitch_class(note, key, scale) for note in self.current.notes] + elif isinstance(self.current, RomanNumeral): + pitch_classes = [ + midi_to_pitch_class(note, key, scale) for note in self.current.notes + ] self.current.set_pitch_classes(pitch_classes) - self.loop_i += 1 return self.current @@ -342,13 +351,13 @@ class Cyclic(Sequence): wrap_end: str = field(default=">", repr=False) def __next__(self): - yield self.values[self.cycle%len(self.cycle)] - self.cycle+=1 + yield self.values[self.cycle % len(self.cycle)] + self.cycle += 1 raise StopIteration def value(self): """Get the value for the current cycle""" - return self.values[self.cycle%len(self.cycle)] + return self.values[self.cycle % len(self.cycle)] @dataclass diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 40953d8..587ca54 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -37,7 +37,7 @@ from .scale import parse_roman, chord_from_roman_numeral class ZiffersTransformer(Transformer): """Rules for transforming Ziffers expressions into tree.""" - def __init__(self, options: Optional[dict]=None): + def __init__(self, options: Optional[dict] = None): super().__init__() self.options = options @@ -114,21 +114,27 @@ class ZiffersTransformer(Transformer): """Parses chord""" return Chord(pitch_classes=items, text="".join([val.text for val in items])) - def named_roman(self,items) -> RomanNumeral: + def named_roman(self, items) -> RomanNumeral: """Parse chord from roman numeral""" numeral = items[0].value - if len(items)>1: + if len(items) > 1: name = items[1] - chord_notes = chord_from_roman_numeral(numeral,name) + chord_notes = chord_from_roman_numeral(numeral, name) parsed_number = parse_roman(numeral) - return RomanNumeral(text=numeral, value=parsed_number, chord_type=name, notes=chord_notes) - return RomanNumeral(value=parse_roman(numeral), text=numeral, notes=chord_from_roman_numeral(numeral)) + return RomanNumeral( + text=numeral, value=parsed_number, chord_type=name, notes=chord_notes + ) + return RomanNumeral( + value=parse_roman(numeral), + text=numeral, + notes=chord_from_roman_numeral(numeral), + ) - def chord_name(self,item): + def chord_name(self, item): """Return name for chord""" return item[0].value - def roman_number(self,item): + def roman_number(self, item): """Return roman numeral""" return item.value @@ -147,7 +153,7 @@ class ZiffersTransformer(Transformer): val = val * (2.0 - (1.0 / (2 * dots))) chars = chars + (dchar + "." * dots) durs = durs + val - return {"text":chars, "duration":durs} + return {"text": chars, "duration": durs} def dchar_not_prefix(self, items): """Return partial duration char info""" @@ -222,9 +228,13 @@ class ZiffersTransformer(Transformer): val = items[0] return Eval(values=val) + def sub_operations(self, items): + """Returns list of operations""" + return Eval(values=items[0], wrap_start="(", wrap_end=")") + def operation(self, items): """Return partial eval operations""" - return items + return flatten(items) def atom(self, token): """Return partial eval item""" diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index e2c0f28..fe81493 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -18,7 +18,7 @@ ?roman_number: /iv|v|v?i{1,3}/ // Valid as integer - ?number: SIGNED_NUMBER | random_integer | cyclic_number + ?number: NUMBER | random_integer | cyclic_number cyclic_number: "<" number (WS number)* ">" // Repeats @@ -69,11 +69,12 @@ // Rules for evaluating clauses inside {} // TODO: Support for parenthesis? - eval: "{" operation "}" - operation: atom (operator atom)+ - atom: (number | DECIMAL) + eval: "{" operation+ "}" + operation: atom (operator (sub_operations | operation))* + sub_operations: "(" operation ")" + atom: (NUMBER | DECIMAL) %import common.NUMBER - %import common.SIGNED_NUMBER + //%import common.SIGNED_NUMBER %import common.DECIMAL %import common.WS \ No newline at end of file From 707e700e3740c27e657960194f68efcd89b2fded Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 12 Feb 2023 00:21:21 +0200 Subject: [PATCH 40/44] mvp random generator & list operation evaluator --- main.py | 2 +- tests/test_parser.py | 4 +- tests/test_scale.py | 8 +- ziffers/classes.py | 475 ++++++++++++++++++++++++++++++++----------- ziffers/defaults.py | 9 + ziffers/mapper.py | 18 +- ziffers/parser.py | 50 +---- ziffers/scale.py | 2 +- ziffers/ziffers.lark | 9 +- 9 files changed, 387 insertions(+), 190 deletions(-) diff --git a/main.py b/main.py index 1ecf2a2..22d14e3 100644 --- a/main.py +++ b/main.py @@ -39,4 +39,4 @@ if __name__ == "__main__": print("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/tests/test_parser.py b/tests/test_parser.py index ed8126b..e1b1620 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -46,8 +46,8 @@ def test_parsing_text(pattern: str): ("q2 eq3", [2, 3]), ], ) -def test_pcs(pattern: str, expected: list): - assert parse_expression(pattern).pcs() == expected +def test_pitch_classes(pattern: str, expected: list): + assert parse_expression(pattern).pitch_classes() == 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]), diff --git a/tests/test_scale.py b/tests/test_scale.py index 23f765b..bc2c7af 100644 --- a/tests/test_scale.py +++ b/tests/test_scale.py @@ -15,11 +15,11 @@ from ziffers import scale ], ) def test_notenames(name: str, expected: int): - assert scale.note_to_midi(name) == expected + assert scale.note_name_to_midi(name) == expected @pytest.mark.parametrize( - "pcs,expected", + "pitch_classes,expected", [ ( list(range(-9, 10)), @@ -47,7 +47,7 @@ def test_notenames(name: str, expected: int): ), ], ) -def test_note_to_midi(pcs: str, expected: int): +def test_note_to_midi(pitch_classes: str, expected: int): assert [ - scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian") for val in pcs + scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian") for val in pitch_classes ] == expected diff --git a/ziffers/classes.py b/ziffers/classes.py index 1a241a4..da55d21 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -1,5 +1,5 @@ """ Ziffers classes for the parsed notation """ -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace import itertools import operator import random @@ -7,10 +7,17 @@ from .defaults import DEFAULT_OPTIONS from .scale import note_from_pc, midi_to_pitch_class -@dataclass +@dataclass(kw_only=True) class Meta: """Abstract class for all Ziffers items""" + kwargs: dict = field(default=None, repr=False) + + def __post_init__(self): + if self.kwargs: + for key, val in self.kwargs.items(): + setattr(self, key, val) + def update(self, new_values): """Update attributes from dict""" for key, value in new_values.items(): @@ -25,21 +32,30 @@ class Meta: setattr(self, key, value) -@dataclass +@dataclass(kw_only=True) class Item(Meta): """Class for all Ziffers text based items""" - text: str + text: str = field(default=None) + + def get_item(self): + """Return the item""" + return self -@dataclass -class Whitespace(Item): +@dataclass(kw_only=True) +class Whitespace: """Class for whitespace""" + text: str item_type: str = field(default=None, repr=False, init=False) + def get_item(self): + """Returns None. Used in filtering""" + return None -@dataclass + +@dataclass(kw_only=True) class DurationChange(Item): """Class for changing duration""" @@ -57,7 +73,7 @@ class OctaveChange(Item): item_type: str = field(default="change", repr=False, init=False) -@dataclass +@dataclass(kw_only=True) class OctaveAdd(Item): """Class for modifying octave""" @@ -66,42 +82,94 @@ class OctaveAdd(Item): item_type: str = field(default="add", repr=False, init=False) -@dataclass +@dataclass(kw_only=True) class Event(Item): """Abstract class for events with duration""" duration: float = field(default=None) -@dataclass +@dataclass(kw_only=True) class Pitch(Event): """Class for pitch in time""" - pitch_class: int = field(default=None) + pitch_class: int octave: int = field(default=None) modifier: int = field(default=0) note: int = field(default=None) + key: str = field(default=None) + scale: str | list = field(default=None) - def set_note(self, note: int): + def __post_init__(self): + super().__post_init__() + if self.text is None: + self.text = str(self.pitch_class) + self.update_note() + + def update_note(self): + """Update note if Key, Scale and Pitch-class is present + """ + if ( + (self.key is not None) + and (self.scale is not None) + and (self.pitch_class is not None) + and (self.note is None) + ): + note = note_from_pc( + root=self.key, + pitch_class=self.pitch_class, + intervals=self.scale, + modifier=self.modifier if self.modifier is not None else 0, + octave=self.octave if self.octave is not None else 0, + ) + self.set_note(note) + + def set_note(self, note: int) -> int: + """Sets a note for the pitch and returns the note. + + Args: + note (int): Midi note + + Returns: + int: Returns the saved note + """ self.note = note return note + # pylint: disable=locally-disabled, unused-argument + def get_value(self, re_eval=False) -> int: + """Returns the pitch class -@dataclass + Returns: + int: Integer value for the pitch + """ + return self.pitch_class + + +@dataclass(kw_only=True) class RandomPitch(Event): """Class for random pitch""" pitch_class: int = field(default=None) + # pylint: disable=locally-disabled, unused-argument + def get_value(self, re_eval=False) -> int: + """Return random value -@dataclass + Returns: + int: Returns random pitch + """ + return self.pitch_class + + +@dataclass(kw_only=True) class RandomPercent(Item): """Class for random percent""" percent: float = field(default=None) -@dataclass +@dataclass(kw_only=True) class Chord(Event): """Class for chords""" @@ -113,7 +181,7 @@ class Chord(Event): self.notes = notes -@dataclass +@dataclass(kw_only=True) class RomanNumeral(Event): """Class for roman numbers""" @@ -123,46 +191,49 @@ class RomanNumeral(Event): pitch_classes: list = None def set_notes(self, chord_notes: list[int]): + """Set notes to roman numeral + + Args: + chord_notes (list[int]): List of notes in midi to be added + """ self.notes = chord_notes def set_pitch_classes(self, pitches: list[tuple]): - if self.pitch_classes == None: + """Set pitch classes to roman numeral + + Args: + pitches (list[tuple]): Pitch classes to be added + """ + if self.pitch_classes is None: self.pitch_classes = [] for pitch in pitches: self.pitch_classes.append(Pitch(**pitch)) -@dataclass +@dataclass(kw_only=True) class Function(Event): """Class for functions""" run: str = field(default=None) -@dataclass +@dataclass(kw_only=True) class Sequence(Meta): """Class for sequences of items""" - values: list[Item] + values: list text: str = field(default=None) wrap_start: str = field(default=None, repr=False) wrap_end: str = field(default=None, repr=False) local_index: int = field(default=0, init=False) + evaluation: bool = field(default=False, init=False) def __post_init__(self): + super().__post_init__() self.text = self.__collect_text() - def __iter__(self): - return self - - def __next__(self): - if self.local_index < len(self.values): - next_item = self.values[self.local_index] - self.local_index += 1 - return next_item - - self.local_index = 0 - raise StopIteration + def __getitem__(self, index): + return self.values[index] def update_values(self, new_values): """Update value attributes from dict""" @@ -180,77 +251,183 @@ class Sequence(Meta): text = text + self.wrap_end return text - def flatten_values(self): - """Flattens the Ziffers object tree""" + def evaluate_tree(self, options=None, re_eval=False): + """Evaluates and flattens the Ziffers object tree""" for item in self.values: if isinstance(item, Sequence): - yield from item.flatten_values() + if item.evaluation: + yield from item.evaluate(options, re_eval) + else: + yield from item.evaluate_tree(options, re_eval) else: - yield item + # Get value / generated value from the item + current = item.get_item() + # Ignore items that returns None + if current is not None: + if isinstance(current, (DurationChange, OctaveChange, OctaveAdd)): + options = self.__update_options(current, options) + else: + if set(("key", "scale")) <= options.keys(): + if isinstance(current, (Pitch, RandomPitch, RandomInteger)): + current = self.__update_pitch(current, options, re_eval) + elif isinstance(current, Chord): + current = self.__update_chord(current, options) + elif isinstance(current, RomanNumeral): + current = self.__create_chord_from_roman( + current, options + ) + current.update_new(options) + yield current + + def filter(self, keep: tuple): + """Filter out items from sequence. + + Args: + keep (tuple): Tuple describing classes to keep + + Returns: + Sequence: Copy of the sequence with filtered values. + """ + return replace( + self, values=[item for item in self.values if isinstance(item, keep)] + ) + + def __update_options(self, current: Item, options: dict) -> dict: + """Update options based on current item + + Args: + current (Item): Current item like Duration change, Octave change etc. + options (dict): Current options + + Returns: + dict: Updated options + """ + if current.item_type == "change": # Change options + options[current.key] = current.value + elif current.item_type == "add": + if current.key in options: # Add to existing value + options[current.key] += current.value + else: # Create value if not existing + options[current.key] = current.value + return options + + def __update_pitch(self, current: Item, options: dict, re_eval: bool = False) -> dict: + """Update pich based on optons + + Args: + current (Item): _description_ + options (dict): _description_ + + Returns: + dict: _description_ + """ + if hasattr(current, "modifier"): + c_modifier = 0 + elif options["modifier"]: + c_modifier = options["modifier"] + else: + c_modifier = 0 + + if hasattr(current, "octave"): + c_octave = 0 + elif options["octave"]: + c_octave = options["octave"] + else: + c_octave = 0 + + note = note_from_pc( + root=options["key"], + pitch_class=current.get_value(re_eval), + intervals=options["scale"], + modifier=c_modifier, + octave=c_octave, + ) + new_pitch = Pitch( + pitch_class=current.get_value(re_eval), + text=str(current.get_value(re_eval)), + note=note, + octave=c_octave, + modifier=c_modifier, + kwargs=options, + ) + return new_pitch + + def __update_chord(self, current: Chord, options: dict) -> Chord: + """Update chord based on options + + Args: + current (Chord): Current chord object + options (dict): Options + re (bool, optional): Re-evaluation flag. Defaults to False. + + Returns: + Chord: Returns updated chord + """ + pcs = current.pitch_classes + notes = [ + pc.set_note(note_from_pc(options["key"], pc.pitch_class, options["scale"])) + for pc in pcs + ] + current.set_notes(notes) + return current + + def __create_chord_from_roman(self, current: RomanNumeral, options: dict) -> Chord: + """Create chord fom roman numeral + + Args: + current (RomanNumeral): Current roman numeral + options (dict): Options + re (bool, optional): Re-evaluation flag. Defaults to False. + + Returns: + Chord: New chord created from Roman numeral + """ + key = options["key"] + scale = options["scale"] + pitches = [midi_to_pitch_class(note, key, scale) for note in current.notes] + chord_notes = [ + note_from_pc( + root=key, + pitch_class=pitch, + intervals=scale, + modifier=current.modifier if hasattr(current, "modifier") else 0, + ) + for pitch in pitches + ] + chord = Chord(text="".join(pitches), pitch_classes=pitches, notes=chord_notes) + return chord -@dataclass +@dataclass(kw_only=True) class Ziffers(Sequence): """Main class for holding options and the current state""" options: dict = field(default_factory=DEFAULT_OPTIONS) loop_i: int = 0 - iterator: iter = field(default=None, repr=False) - current: Whitespace | DurationChange | OctaveChange | OctaveAdd = field( - default=None - ) + iterator = None + current: Item = field(default=None) - def __post_init__(self): - super().__post_init__() - self.iterator = self.flatten_values() + def __iter__(self): + return self def __next__(self): self.current = next(self.iterator) - - # Skip whitespace and collect duration & octave changes - while isinstance( - self.current, (Whitespace, DurationChange, OctaveChange, OctaveAdd) - ): - if self.current.item_type == "change": # Change options - self.options[self.current.key] = self.current.value - elif self.current.item_type == "add": - if self.current.key in self.options: # Add to existing value - self.options[self.current.key] += self.current.value - else: # Create value if not existing - self.options[self.current.key] = self.current.value - - self.current = next(self.iterator) # Skip item - - # Update collected options & default options - self.current.update_new(self.options) - - # Resolve note(s) from scale - if set(("key", "scale")) <= self.options.keys(): - key = self.options["key"] - scale = self.options["scale"] - if isinstance(self.current, (Pitch, RandomPitch)): - note = note_from_pc( - root=key, - pitch_class=self.current.pitch_class, - intervals=scale, - modifier=self.current.modifier, - ) - self.current.set_note(note) - elif isinstance(self.current, Chord): - pcs = self.current.pitch_classes - notes = [ - pc.set_note(note_from_pc(key, pc.pitch_class, scale)) for pc in pcs - ] - self.current.set_notes(notes) - elif isinstance(self.current, RomanNumeral): - pitch_classes = [ - midi_to_pitch_class(note, key, scale) for note in self.current.notes - ] - self.current.set_pitch_classes(pitch_classes) - self.loop_i += 1 return self.current + def init_opts(self, options): + """Evaluate the Ziffers tree using the options""" + self.options = options + self.iterator = iter(self.evaluate_tree(self.options)) + + def re_eval(self, options): + """Re-evaluate the iterator""" + self.iterator = iter(self.evaluate_tree(options, True)) + + def get_list(self): + """Return list""" + return list(self) + def take(self, num: int) -> list[Pitch]: """Take number of pitch classes from the parsed sequence. Cycles from the beginning. @@ -263,7 +440,8 @@ class Ziffers(Sequence): return list(itertools.islice(itertools.cycle(self), num)) def loop(self) -> iter: - return itertools.cycle(self.iterator) + """Return cyclic loop""" + return itertools.cycle(iter(self)) def set_defaults(self, options: dict): """Sets options for the parser @@ -293,7 +471,7 @@ class Ziffers(Sequence): return [val.octave for val in self.values if isinstance(val, Pitch)] -@dataclass +@dataclass(kw_only=True) class ListSequence(Sequence): """Class for Ziffers list sequences""" @@ -301,14 +479,19 @@ class ListSequence(Sequence): wrap_end: str = field(default=")", repr=False) -@dataclass +@dataclass(kw_only=True) class Integer(Item): """Class for integers""" value: int + # pylint: disable=locally-disabled, unused-argument + def get_value(self, re_eval=False): + """Return value of the integer""" + return self.value -@dataclass + +@dataclass(kw_only=True) class RandomInteger(Item): """Class for random integer""" @@ -316,17 +499,19 @@ class RandomInteger(Item): max: int def __post_init__(self): + super().__post_init__() if self.min > self.max: new_max = self.min self.min = self.max self.max = new_max - def value(self): + # pylint: disable=locally-disabled, unused-argument + def get_value(self, re_eval=False): """Evaluate the random value for the generator""" return random.randint(self.min, self.max) -@dataclass +@dataclass(kw_only=True) class RepeatedListSequence(Sequence): """Class for Ziffers list sequences""" @@ -335,32 +520,47 @@ class RepeatedListSequence(Sequence): wrap_end: str = field(default=":)", repr=False) -@dataclass +@dataclass(kw_only=True) class Subdivision(Item): """Class for subdivisions""" values: list[Event] -@dataclass -class Cyclic(Sequence): +@dataclass(kw_only=True) +class Cyclic(Item): """Class for cyclic sequences""" + values: list cycle: int = 0 wrap_start: str = field(default="<", repr=False) wrap_end: str = field(default=">", repr=False) - def __next__(self): - yield self.values[self.cycle % len(self.cycle)] - self.cycle += 1 - raise StopIteration + def __post_init__(self): + super().__post_init__() + self.text = self.__collect_text() + self.values = [val for val in self.values if not isinstance(val, Whitespace)] - def value(self): + def __collect_text(self) -> str: + """Collect text value from values""" + text = "".join([val.text for val in self.values]) + if self.wrap_start is not None: + text = self.wrap_start + text + if self.wrap_end is not None: + text = text + self.wrap_end + return text + + def get_value(self, re_eval=False): """Get the value for the current cycle""" - return self.values[self.cycle % len(self.cycle)] + value = self.values[self.cycle % len(self.values)] + if re_eval: # If re-evaluated + self.cycle = 0 + else: + self.cycle += 1 + return value -@dataclass +@dataclass(kw_only=True) class Range(Item): """Class for range""" @@ -368,35 +568,62 @@ class Range(Item): end: int = field(default=None) -ops = { - "+": operator.add, - "-": operator.sub, - "*": operator.mul, - "/": operator.truediv, - "%": operator.mod, -} - - -@dataclass +@dataclass(kw_only=True) class Operator(Item): """Class for math operators""" - value: ... = field(init=False, repr=False) - - def __post_init__(self): - self.value = ops[self.text] + value: ... -@dataclass +@dataclass(kw_only=True) class ListOperation(Sequence): """Class for list operations""" - def run(self): - """Run operations""" - pass + def __post_init__(self): + super().__post_init__() + self.evaluation = True + + def filter_operation(self, values): + """Filtering for the operation elements""" + keep = (Sequence, Event, RandomInteger, Integer, Cyclic) + for item in values: + if isinstance(item, Sequence): + yield item.filter(keep) + elif isinstance(item, keep): + yield item + + def evaluate(self, options: dict, re_eval=False): + """Evaluates the operation""" + operators = self.values[1::2] # Fetch every second operator element + values = self.values[::2] # Fetch every second list element + values = list(self.filter_operation(values)) # Filter out crap + result = values[0] # Start results with the first array + for i, operand in enumerate(operators): + operation = operand.value + right_value = values[i + 1] + if isinstance(right_value, Sequence): + result = [ + Pitch( + pitch_class=operation(x.get_value(re_eval), y.get_value(re_eval)), + kwargs=options, + ) + for x in result + for y in right_value + ] + else: + result = [ + Pitch( + pitch_class=operation( + x.get_value(re_eval), right_value.get_value(re_eval) + ), + kwargs=options, + ) + for x in result + ] + return Sequence(values=result) -@dataclass +@dataclass(kw_only=True) class Operation(Item): """Class for lisp-like operations: (+ 1 2 3) etc.""" @@ -404,7 +631,7 @@ class Operation(Item): operator: operator -@dataclass +@dataclass(kw_only=True) class Eval(Sequence): """Class for evaluation notation""" @@ -417,14 +644,14 @@ class Eval(Sequence): self.result = eval(self.text) -@dataclass +@dataclass(kw_only=True) class Atom(Item): """Class for evaluable atoms""" value: ... -@dataclass +@dataclass(kw_only=True) class Euclid(Item): """Class for euclidean cycles""" @@ -435,7 +662,7 @@ class Euclid(Item): rotate: int = field(default=None) -@dataclass +@dataclass(kw_only=True) class RepeatedSequence(Sequence): """Class for repeats""" diff --git a/ziffers/defaults.py b/ziffers/defaults.py index e4062db..f404421 100644 --- a/ziffers/defaults.py +++ b/ziffers/defaults.py @@ -1,4 +1,5 @@ """ Default options for Ziffers """ +import operator DEFAULT_DURS = { "m": 8.0, # 15360/1920 @@ -82,6 +83,14 @@ MODIFIERS = { ROMANS = {"i": 1, "v": 5, "x": 10, "l": 50, "c": 100, "d": 500, "m": 1000} +OPERATORS = { + "+": operator.add, + "-": operator.sub, + "*": operator.mul, + "/": operator.truediv, + "%": operator.mod, +} + # pylint: disable=locally-disabled, too-many-lines SCALES = { diff --git a/ziffers/mapper.py b/ziffers/mapper.py index 587ca54..2a7dcda 100644 --- a/ziffers/mapper.py +++ b/ziffers/mapper.py @@ -29,7 +29,7 @@ from .classes import ( RepeatedSequence, ) from .common import flatten, sum_dict -from .defaults import DEFAULT_DURS +from .defaults import DEFAULT_DURS, OPERATORS from .scale import parse_roman, chord_from_roman_numeral @@ -43,8 +43,8 @@ class ZiffersTransformer(Transformer): def start(self, items) -> Ziffers: """Root for the rules""" - seq = Sequence(values=items[0]) - return Ziffers(values=seq, options={}) + # seq = Sequence(values=items[0]) + return Ziffers(values=items[0], options={}) def sequence(self, items): """Flatten sequence""" @@ -53,7 +53,7 @@ class ZiffersTransformer(Transformer): def random_integer(self, item) -> RandomInteger: """Parses random integer syntax""" val = item[0][1:-1].split(",") - return RandomInteger(min=val[0], max=val[1], text=item[0].value) + return RandomInteger(min=int(val[0]), max=int(val[1]), text=item[0].value) def range(self, item) -> Range: """Parses range syntax""" @@ -284,14 +284,14 @@ class ZiffersTransformer(Transformer): ) return seq - def SIGNED_NUMBER(self, token): + def NUMBER(self, token): """Parse integer""" val = token.value return Integer(text=val, value=int(val)) def number(self, item): """Return partial number (Integer or RandomInteger)""" - return item + return item[0] def cyclic_number(self, item): """Parse cyclic notation""" @@ -310,7 +310,7 @@ class ZiffersTransformer(Transformer): def operator(self, token): """Parse operator""" val = token[0].value - return Operator(text=val) + return Operator(text=val, value=OPERATORS[val]) def list_items(self, items): """Parse sequence""" @@ -320,6 +320,10 @@ class ZiffersTransformer(Transformer): """Parse list operation""" return ListOperation(values=items) + def right_op(self,items): + """Get right value for the operation""" + return items[0] + def euclid(self, items): """Parse euclid notation""" params = items[1][1:-1].split(",") diff --git a/ziffers/parser.py b/ziffers/parser.py index 683d5e0..4e5c7dd 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -41,58 +41,14 @@ def zparse(expr: str, **opts) -> Ziffers: """ parsed = parse_expression(expr) if opts: - parsed.set_defaults(opts) + parsed.init_opts(opts) return parsed # pylint: disable=invalid-name -def z0(expr: str, **opts) -> Ziffers: - """Shortened method name for zparse""" - return zparse(expr, **opts) - - -def z1(expr: str, **opts) -> Ziffers: - """Shortened method name for zparse""" - return zparse(expr, **opts) - - -def z2(expr: str, **opts) -> Ziffers: - """Shortened method name for zparse""" - return zparse(expr, **opts) - - -def z3(expr: str, **opts) -> Ziffers: - """Shortened method name for zparse""" - return zparse(expr, **opts) - - -def z4(expr: str, **opts) -> Ziffers: - """Shortened method name for zparse""" - return zparse(expr, **opts) - - -def z5(expr: str, **opts) -> Ziffers: - """Shortened method name for zparse""" - return zparse(expr, **opts) - - -def z6(expr: str, **opts) -> Ziffers: - """Shortened method name for zparse""" - return zparse(expr, **opts) - - -def z7(expr: str, **opts) -> Ziffers: - """Shortened method name for zparse""" - return zparse(expr, **opts) - - -def z8(expr: str, **opts) -> Ziffers: - """Shortened method name for zparse""" - return zparse(expr, **opts) - - -def z9(expr: str, **opts) -> Ziffers: +def z(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" return zparse(expr, **opts) + \ No newline at end of file diff --git a/ziffers/scale.py b/ziffers/scale.py index bc24428..2d8f035 100644 --- a/ziffers/scale.py +++ b/ziffers/scale.py @@ -209,7 +209,7 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict: scale (str): Used scale Returns: - tuple: Returns tuple containing (pitch class as string, pitch class, octave, optional modifier) + tuple: Returns dict containing pitch-class values """ pitch_class = note % 12 octave = midi_to_octave(note) - 5 diff --git a/ziffers/ziffers.lark b/ziffers/ziffers.lark index fe81493..bccf0e1 100644 --- a/ziffers/ziffers.lark +++ b/ziffers/ziffers.lark @@ -18,7 +18,7 @@ ?roman_number: /iv|v|v?i{1,3}/ // Valid as integer - ?number: NUMBER | random_integer | cyclic_number + number: NUMBER | random_integer | cyclic_number cyclic_number: "<" number (WS number)* ">" // Repeats @@ -29,7 +29,8 @@ repeated_list: prefix* "(:" sequence ":" [number] ")" // Right recursive list operation - list_op: list (operator (list | number))+ + list_op: list (operator right_op)+ + right_op: list | number operator: /([\+\-\*\/%]|<<|>>)/ // Euclidean cycles @@ -64,8 +65,8 @@ random_integer: /\(-?[0-9]+,-?[0-9]+\)/ range: /-?[0-9]\.\.-?[0-9]/ cycle: "<" sequence ">" - random_pitch: "?" - random_percent: "%" + random_pitch: /(\?)(?!\d)/ + random_percent: /(%)(?!\d)/ // Rules for evaluating clauses inside {} // TODO: Support for parenthesis? From d91e9d230a07809f0e27ad8ecd12de2d05748512 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 12 Feb 2023 00:29:53 +0200 Subject: [PATCH 41/44] Changed checks for dicts --- ziffers/classes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index da55d21..8696e50 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -323,14 +323,14 @@ class Sequence(Meta): """ if hasattr(current, "modifier"): c_modifier = 0 - elif options["modifier"]: + elif "modifier" in options: c_modifier = options["modifier"] else: c_modifier = 0 if hasattr(current, "octave"): c_octave = 0 - elif options["octave"]: + elif "octave" in options: c_octave = options["octave"] else: c_octave = 0 From bd911b96cf1b04f2b812db70606d1754d00ea442 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 12 Feb 2023 01:09:54 +0200 Subject: [PATCH 42/44] Added some support for cycles --- ziffers/classes.py | 56 +++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index 8696e50..6dab368 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -137,7 +137,7 @@ class Pitch(Event): return note # pylint: disable=locally-disabled, unused-argument - def get_value(self, re_eval=False) -> int: + def get_value(self) -> int: """Returns the pitch class Returns: @@ -153,7 +153,7 @@ class RandomPitch(Event): pitch_class: int = field(default=None) # pylint: disable=locally-disabled, unused-argument - def get_value(self, re_eval=False) -> int: + def get_value(self) -> int: """Return random value Returns: @@ -251,14 +251,14 @@ class Sequence(Meta): text = text + self.wrap_end return text - def evaluate_tree(self, options=None, re_eval=False): + def evaluate_tree(self, options=None): """Evaluates and flattens the Ziffers object tree""" for item in self.values: if isinstance(item, Sequence): if item.evaluation: - yield from item.evaluate(options, re_eval) + yield from item.evaluate(options) else: - yield from item.evaluate_tree(options, re_eval) + yield from item.evaluate_tree(options) else: # Get value / generated value from the item current = item.get_item() @@ -268,8 +268,10 @@ class Sequence(Meta): options = self.__update_options(current, options) else: if set(("key", "scale")) <= options.keys(): + if isinstance(current,Cyclic): + current = current.get_value() if isinstance(current, (Pitch, RandomPitch, RandomInteger)): - current = self.__update_pitch(current, options, re_eval) + current = self.__update_pitch(current, options) elif isinstance(current, Chord): current = self.__update_chord(current, options) elif isinstance(current, RomanNumeral): @@ -311,7 +313,7 @@ class Sequence(Meta): options[current.key] = current.value return options - def __update_pitch(self, current: Item, options: dict, re_eval: bool = False) -> dict: + def __update_pitch(self, current: Item, options: dict) -> dict: """Update pich based on optons Args: @@ -337,14 +339,14 @@ class Sequence(Meta): note = note_from_pc( root=options["key"], - pitch_class=current.get_value(re_eval), + pitch_class=current.get_value(), intervals=options["scale"], modifier=c_modifier, octave=c_octave, ) new_pitch = Pitch( - pitch_class=current.get_value(re_eval), - text=str(current.get_value(re_eval)), + pitch_class=current.get_value(), + text=str(current.get_value()), note=note, octave=c_octave, modifier=c_modifier, @@ -415,14 +417,21 @@ class Ziffers(Sequence): self.loop_i += 1 return self.current - def init_opts(self, options): + # pylint: disable=locally-disabled, dangerous-default-value + def init_opts(self, options=None): """Evaluate the Ziffers tree using the options""" - self.options = options + if options is None: + self.options = DEFAULT_OPTIONS + else: + self.options.update(options) + self.iterator = iter(self.evaluate_tree(self.options)) - def re_eval(self, options): + def re_eval(self, options=None): """Re-evaluate the iterator""" - self.iterator = iter(self.evaluate_tree(options, True)) + if options is not None: + self.options.update(options) + self.iterator = iter(self.evaluate_tree(self.options)) def get_list(self): """Return list""" @@ -486,7 +495,7 @@ class Integer(Item): value: int # pylint: disable=locally-disabled, unused-argument - def get_value(self, re_eval=False): + def get_value(self): """Return value of the integer""" return self.value @@ -506,7 +515,7 @@ class RandomInteger(Item): self.max = new_max # pylint: disable=locally-disabled, unused-argument - def get_value(self, re_eval=False): + def get_value(self): """Evaluate the random value for the generator""" return random.randint(self.min, self.max) @@ -550,13 +559,10 @@ class Cyclic(Item): text = text + self.wrap_end return text - def get_value(self, re_eval=False): - """Get the value for the current cycle""" + def get_value(self): + """Get the value for the current cycle""" value = self.values[self.cycle % len(self.values)] - if re_eval: # If re-evaluated - self.cycle = 0 - else: - self.cycle += 1 + self.cycle += 1 return value @@ -592,7 +598,7 @@ class ListOperation(Sequence): elif isinstance(item, keep): yield item - def evaluate(self, options: dict, re_eval=False): + def evaluate(self, options: dict): """Evaluates the operation""" operators = self.values[1::2] # Fetch every second operator element values = self.values[::2] # Fetch every second list element @@ -604,7 +610,7 @@ class ListOperation(Sequence): if isinstance(right_value, Sequence): result = [ Pitch( - pitch_class=operation(x.get_value(re_eval), y.get_value(re_eval)), + pitch_class=operation(x.get_value(), y.get_value()), kwargs=options, ) for x in result @@ -614,7 +620,7 @@ class ListOperation(Sequence): result = [ Pitch( pitch_class=operation( - x.get_value(re_eval), right_value.get_value(re_eval) + x.get_value(), right_value.get_value() ), kwargs=options, ) From e08c172f68cb20dd5d8e73bf3354f1e292d68f35 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 12 Feb 2023 01:24:43 +0200 Subject: [PATCH 43/44] Fixed bug with empty options --- ziffers/classes.py | 2 +- ziffers/parser.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index 6dab368..d5d3ad6 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -420,7 +420,7 @@ class Ziffers(Sequence): # pylint: disable=locally-disabled, dangerous-default-value def init_opts(self, options=None): """Evaluate the Ziffers tree using the options""" - if options is None: + if options is None or len(options)<=0: self.options = DEFAULT_OPTIONS else: self.options.update(options) diff --git a/ziffers/parser.py b/ziffers/parser.py index 4e5c7dd..1b83122 100644 --- a/ziffers/parser.py +++ b/ziffers/parser.py @@ -40,8 +40,7 @@ def zparse(expr: str, **opts) -> Ziffers: Ziffers: Returns Ziffers iterable parsed with the given options """ parsed = parse_expression(expr) - if opts: - parsed.init_opts(opts) + parsed.init_opts(opts) return parsed @@ -51,4 +50,3 @@ def zparse(expr: str, **opts) -> Ziffers: def z(expr: str, **opts) -> Ziffers: """Shortened method name for zparse""" return zparse(expr, **opts) - \ No newline at end of file From 666aec37672aae39b4b9e49f71586198df25c51c Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 12 Feb 2023 01:38:27 +0200 Subject: [PATCH 44/44] Changed check for default opts --- ziffers/classes.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/ziffers/classes.py b/ziffers/classes.py index d5d3ad6..d4f763e 100644 --- a/ziffers/classes.py +++ b/ziffers/classes.py @@ -107,8 +107,7 @@ class Pitch(Event): self.update_note() def update_note(self): - """Update note if Key, Scale and Pitch-class is present - """ + """Update note if Key, Scale and Pitch-class is present""" if ( (self.key is not None) and (self.scale is not None) @@ -268,7 +267,7 @@ class Sequence(Meta): options = self.__update_options(current, options) else: if set(("key", "scale")) <= options.keys(): - if isinstance(current,Cyclic): + if isinstance(current, Cyclic): current = current.get_value() if isinstance(current, (Pitch, RandomPitch, RandomInteger)): current = self.__update_pitch(current, options) @@ -420,16 +419,18 @@ class Ziffers(Sequence): # pylint: disable=locally-disabled, dangerous-default-value def init_opts(self, options=None): """Evaluate the Ziffers tree using the options""" - if options is None or len(options)<=0: - self.options = DEFAULT_OPTIONS - else: + self.options.update(DEFAULT_OPTIONS) + if options: self.options.update(options) - + else: + self.options = DEFAULT_OPTIONS + self.iterator = iter(self.evaluate_tree(self.options)) def re_eval(self, options=None): """Re-evaluate the iterator""" - if options is not None: + self.options.update(DEFAULT_OPTIONS) + if options: self.options.update(options) self.iterator = iter(self.evaluate_tree(self.options)) @@ -560,7 +561,7 @@ class Cyclic(Item): return text def get_value(self): - """Get the value for the current cycle""" + """Get the value for the current cycle""" value = self.values[self.cycle % len(self.values)] self.cycle += 1 return value @@ -619,9 +620,7 @@ class ListOperation(Sequence): else: result = [ Pitch( - pitch_class=operation( - x.get_value(), right_value.get_value() - ), + pitch_class=operation(x.get_value(), right_value.get_value()), kwargs=options, ) for x in result