diff --git a/.gitignore b/.gitignore index 16d04f22..f2b60df4 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,7 @@ DerivedData generators/vs2015/Heavy/generated generators/c2js/emsdk_portable/ generators/c2js/emsdk/ + +## added by dgbillotte to ignore my dev stuff, feel free to remove +test/pd/*/dgb* + diff --git a/hvcc/core/hv2ir/HIrExpr.py b/hvcc/core/hv2ir/HIrExpr.py new file mode 100644 index 00000000..288230b4 --- /dev/null +++ b/hvcc/core/hv2ir/HIrExpr.py @@ -0,0 +1,34 @@ +# Copyright (C) 2014-2018 Enzien Audio, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .HeavyIrObject import HeavyIrObject +from .HeavyException import HeavyException + + +class HIrExpr(HeavyIrObject): + """Just a stub to get it going...""" + + def __init__(self, obj_type, args=None, graph=None, annotations=None): + HeavyIrObject.__init__(self, obj_type, args=args, graph=graph, + num_inlets=args["num_inlets"], + num_outlets=1, + annotations=annotations) + + def inlet_requires_signal(self, inlet_index=0): + """ Overriding HeavyIrObject to deal with variable number of inlets + """ + if inlet_index >= self.num_inlets: + raise HeavyException(f"inlet_index: {inlet_index} is greater than number of inlets") + return True diff --git a/hvcc/core/hv2ir/HeavyLangObject.py b/hvcc/core/hv2ir/HeavyLangObject.py index f8d3b40f..023dde2f 100644 --- a/hvcc/core/hv2ir/HeavyLangObject.py +++ b/hvcc/core/hv2ir/HeavyLangObject.py @@ -75,6 +75,7 @@ def __init__( # the list of connections at each inlet num_inlets = num_inlets if num_inlets >= 0 else len(self._obj_desc["inlets"]) + # print("=-=-=-=- HeavyLangObject::__init__", self.type, self._obj_desc) self.inlet_connections: List = [[] for _ in range(num_inlets)] # the list of connections at each outlet @@ -224,6 +225,8 @@ def add_connection(self, c: Connection) -> None: """ try: if c.to_object is self: + print("-------- c props", c) + print("-------- inlet cons", self.inlet_connections) self.inlet_connections[c.inlet_index].append(c) elif c.from_object is self: self.outlet_connections[c.outlet_index].append(c) diff --git a/hvcc/core/hv2ir/HeavyParser.py b/hvcc/core/hv2ir/HeavyParser.py index 180de62d..20a24218 100644 --- a/hvcc/core/hv2ir/HeavyParser.py +++ b/hvcc/core/hv2ir/HeavyParser.py @@ -21,6 +21,7 @@ from typing import Any, Dict, Optional from .HIrConvolution import HIrConvolution +from .HIrExpr import HIrExpr from .HIrInlet import HIrInlet from .HIrLorenz import HIrLorenz from .HIrOutlet import HIrOutlet @@ -310,5 +311,7 @@ def reduce(self) -> tuple: "send": HLangSend, "__switchcase": HIrSwitchcase, "switchcase": HIrSwitchcase, - "__pack": HIrPack + "__pack": HIrPack, + "__expr": HIrExpr, + "__expr~": HIrExpr, } diff --git a/hvcc/core/json/heavy.ir.json b/hvcc/core/json/heavy.ir.json index ff36a44d..fb0db498 100644 --- a/hvcc/core/json/heavy.ir.json +++ b/hvcc/core/json/heavy.ir.json @@ -1037,6 +1037,56 @@ ] }, + "__expr": { + "inlets": [], + "ir": { + "control": true, + "signal": false, + "init": true + }, + "outlets": [ + "-->" + ], + "args": [{ + "name": "expressions", + "value_type": "stringarray", + "description": "The expression to evaluate", + "default": "", + "required": true + }, + { + "name": "num_inlets", + "value_type": "int", + "description": "Number of inlets for this instance", + "default": "", + "required": true + }] + }, + "__expr~": { + "inlets": [], + "ir": { + "control": false, + "signal": true, + "init": false + }, + "outlets": [ + "~f>" + ], + "args": [{ + "name": "expressions", + "value_type": "stringarray", + "description": "The expression to evaluate", + "default": "", + "required": true + }, + { + "name": "num_inlets", + "value_type": "int", + "description": "Number of inlets for this instance", + "default": "", + "required": true + }] + }, "__floor": { "inlets": [ "-->" @@ -1049,9 +1099,7 @@ "outlets": [ "-->" ], - "args": [ - - ] + "args": [] }, "__floor~f": { "args": [ diff --git a/hvcc/generators/ir2c/ControlDelay.py b/hvcc/generators/ir2c/ControlDelay.py index 591f6438..e9929ddc 100644 --- a/hvcc/generators/ir2c/ControlDelay.py +++ b/hvcc/generators/ir2c/ControlDelay.py @@ -62,7 +62,8 @@ def get_C_impl( obj_id: int, on_message_list: List, get_obj_class: Callable, - objects: Dict + objects: Dict, + args: Dict ) -> List[str]: send_message_list = [ f"cDelay_{obj_id}_sendMessage(HeavyContextInterface *_c, int letIn, const HvMessage *const m) {{" diff --git a/hvcc/generators/ir2c/ControlExpr.py b/hvcc/generators/ir2c/ControlExpr.py new file mode 100644 index 00000000..60181215 --- /dev/null +++ b/hvcc/generators/ir2c/ControlExpr.py @@ -0,0 +1,127 @@ +# Copyright (C) 2014-2018 Enzien Audio, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .HeavyObject import HeavyObject +import re +from typing import Callable, Dict, List + + +class ControlExpr(HeavyObject): + """Just a stub to get the thing working""" + + c_struct = "ControlExpr" + preamble = "cExpr" + + @classmethod + def get_C_header_set(self) -> set: + return {"HvControlExpr.h"} + + @classmethod + def get_C_file_set(self) -> set: + return {"HvControlExpr.h", "HvControlExpr.c"} + + @classmethod + def get_C_init(cls, obj_type: str, obj_id: int, args: Dict) -> List[str]: + """(Per object) code that gets inserted into from the Heavy_heavy ctor. + Only if "ir[init]" == true + """ + + eval_f = f"&Heavy_heavy::{cls.preamble}_{obj_id}_evaluate" + return [f"cExpr_init(&cExpr_{obj_id}, {eval_f});"] + + @classmethod + def get_C_def(cls, obj_type: str, obj_id: int) -> List[str]: + """(Per object) code that gets inserted into the header file + Only if "ir[init]" == true + """ + + lines = super().get_C_def(obj_type, obj_id) + lines.append("// --------------- big ol' comment ------------") + lines.append(f"static float {cls.preamble}_{obj_id}_evaluate(float* args);") + return lines + + @classmethod + def get_C_onMessage(cls, obj_type: str, obj_id: int, inlet_index: int, args: Dict) -> List[str]: + """ + (Per object) code that gets inserted into the c__sendMessage + method in the .cpp file + """ + + return [ + "cExpr_onMessage(_c, &Context(_c)->cExpr_{0}, {1}, m, &cExpr_{0}_sendMessage);".format( + obj_id, + inlet_index) + ] + """ + The get_C_onMessage method returns the code that will get inserted into + the cReceive__sendMessage method + """ + + # @classmethod + # def get_C_process(cls, process_dict: Dict, obj_type: str, obj_id: int, args: Dict) -> List[str]: + # return [ + # "printf(\"hello world\")" + # ] + """ + The get_C_process method seems to only get called by Signal IR objects, it does + not get called for Control IR objects + """ + + @classmethod + def get_C_impl( + cls, + obj_type: str, + obj_id: int, + on_message_list: List, + get_obj_class: Callable, + objects: Dict, + args: Dict + ) -> List[str]: + """ + (Per object) this creates the _sendMessage function that other objects use to + send messages to this object. + """ + + lines = super().get_C_impl(obj_type, obj_id, on_message_list, get_obj_class, objects, args) + expr = args["expressions"][0] + bound_expr = bind_expr(expr, "args") + lines.extend([ + "", + f"float Heavy_heavy::{cls.preamble}_{obj_id}_evaluate(float* args) {{", + f"\treturn {bound_expr};", + "}", + ]) + return lines + + +""" +Below is code to rewrite the input expression into one that uses local variables +that have been cast to either float or int +""" + + +# todo(dgb): need to handle the 's' type +def var_n(a_name, var): + parts = re.match(r"\$([fi])(\d)", var) + type = "float" if parts[1] == "f" else "int" + return f"(({type})({a_name}[{int(parts[2])-1}]))" + + +def bind_expr(exp="$f1+2", a_name="a"): + vars = re.findall(r"\$[fis]\d", exp) + new_exp = exp + for var in vars: + new_exp = new_exp.replace(var, var_n(a_name, var)) + return new_exp diff --git a/hvcc/generators/ir2c/ControlMessage.py b/hvcc/generators/ir2c/ControlMessage.py index ae946bb3..745c13d7 100644 --- a/hvcc/generators/ir2c/ControlMessage.py +++ b/hvcc/generators/ir2c/ControlMessage.py @@ -35,7 +35,8 @@ def get_C_impl( obj_id: int, on_message_list: List, get_obj_class: Callable, - objects: Dict + objects: Dict, + args: Dict ) -> List[str]: send_message_list = [ f"cMsg_{obj_id}_sendMessage(HeavyContextInterface *_c, int letIn, const HvMessage *const n) {{" diff --git a/hvcc/generators/ir2c/ControlPrint.py b/hvcc/generators/ir2c/ControlPrint.py index 62d43dfb..81a7ee41 100644 --- a/hvcc/generators/ir2c/ControlPrint.py +++ b/hvcc/generators/ir2c/ControlPrint.py @@ -55,6 +55,7 @@ def get_C_impl( obj_id: int, on_message_list: List, get_obj_class: Callable, - objects: Dict + objects: Dict, + args: Dict ) -> List[str]: return [] diff --git a/hvcc/generators/ir2c/ControlSend.py b/hvcc/generators/ir2c/ControlSend.py index 39e82ce6..ee3b5a12 100644 --- a/hvcc/generators/ir2c/ControlSend.py +++ b/hvcc/generators/ir2c/ControlSend.py @@ -35,7 +35,8 @@ def get_C_impl( obj_id: int, on_message_list: List, get_obj_class: Callable, - objects: Dict + objects: Dict, + args: Dict ) -> List[str]: # Note(joe): if no corresponding receivers exist and there's no extern indicator # then there is not much need to generate code stub diff --git a/hvcc/generators/ir2c/ControlSwitchcase.py b/hvcc/generators/ir2c/ControlSwitchcase.py index c1d1b05d..27582b35 100644 --- a/hvcc/generators/ir2c/ControlSwitchcase.py +++ b/hvcc/generators/ir2c/ControlSwitchcase.py @@ -49,7 +49,8 @@ def get_C_impl( obj_id: int, on_message_list: List, get_obj_class: Callable, - objects: Dict + objects: Dict, + args: Dict ) -> List[str]: # generate the onMessage implementation out_list = [ diff --git a/hvcc/generators/ir2c/HeavyObject.py b/hvcc/generators/ir2c/HeavyObject.py index 4845a819..a32122e7 100644 --- a/hvcc/generators/ir2c/HeavyObject.py +++ b/hvcc/generators/ir2c/HeavyObject.py @@ -47,6 +47,10 @@ def get_C_header_set(self) -> set: def get_C_file_set(self) -> set: return set() + @classmethod + def get_C_init(cls, obj_type: str, obj_id: int, args: Dict) -> List[str]: + return [f"{cls.preamble}_init(&{cls.preamble}_{obj_id})"] + @classmethod def get_C_def(cls, obj_type: str, obj_id: int) -> List[str]: return ["{0} {1}_{2};".format( @@ -73,7 +77,8 @@ def get_C_impl( obj_id: int, on_message_list: List, get_obj_class: Callable, - objects: Dict + objects: Dict, + args: Dict ) -> List[str]: send_message_list = [ "{0}_{1}_sendMessage(HeavyContextInterface *_c, int letIn, const HvMessage *m) {{".format( @@ -97,12 +102,24 @@ def get_C_impl( return send_message_list @classmethod - def get_C_init(cls, obj_type: str, obj_id: int, args: Dict) -> List[str]: - raise NotImplementedError("method get_C_init not implemented") + def get_C_onMessage(cls, obj_type: str, obj_id: int, inlet_index: int, args: Dict) -> List[str]: + raise NotImplementedError("method get_C_onMessage not implemented", cls, obj_type) @classmethod - def get_C_onMessage(cls, obj_type: str, obj_id: int, inlet_index: int, args: Dict) -> List[str]: - raise NotImplementedError("method get_C_onMessage not implemented") + def get_C_obj_header_code(cls, obj_type: str, obj_id: int, args: Dict) -> List[str]: + return [] + + @classmethod + def get_C_obj_impl_code(cls, obj_type: str, obj_id: int, args: Dict) -> List[str]: + return [] + + @classmethod + def get_C_class_header_code(cls, obj_type: str, args: Dict) -> List[str]: + return [] + + @classmethod + def get_C_class_impl_code(cls, obj_type: str, args: Dict) -> List[str]: + return [] @classmethod def get_C_process(cls, process_dict: Dict, obj_type: str, obj_id: int, args: Dict) -> List[str]: diff --git a/hvcc/generators/ir2c/SignalExpr.py b/hvcc/generators/ir2c/SignalExpr.py new file mode 100644 index 00000000..6e477917 --- /dev/null +++ b/hvcc/generators/ir2c/SignalExpr.py @@ -0,0 +1,110 @@ +# Copyright (C) 2014-2018 Enzien Audio, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from typing import Dict, List + +from .expr_c_writer import ExprCWriter +from .HeavyObject import HeavyObject + + +class SignalExpr(HeavyObject): + """Handles the math objects. + """ + + preamble = "cExprSig" + + obj_eval_functions: Dict = {} + + @classmethod + def handles_type(cls, obj_type: str) -> bool: + """Returns true if the object type can be handled by this class + """ + return obj_type == "_expr~" + + @classmethod + def get_C_header_set(self) -> set: + return {"HvMath.h"} + + @classmethod + def get_C_class_header_code(cls, obj_type: str, args: Dict) -> List[str]: + eval_funcs = ", ".join(cls.obj_eval_functions.values()) + fptr_type = f"{cls.preamble}_evaluator" + lines = [ + f"typedef void(*{fptr_type})(hv_bInf_t*, hv_bOutf_t);", + f"{fptr_type} {cls.preamble}_evaluators[{len(cls.obj_eval_functions)}] = {{{eval_funcs}}};", + ] + return lines + + @classmethod + def get_C_obj_header_code(cls, obj_type: str, obj_id: int, args: Dict) -> List[str]: + lines = super().get_C_obj_header_code(obj_type, obj_id, args) + func_name = f"{cls.preamble}_{obj_id}_evaluate" + cls.obj_eval_functions[obj_id] = func_name + lines.extend([ + f"static inline void {func_name}(hv_bInf_t* bIns, hv_bOutf_t bOut);", + ]) + return lines + + @classmethod + def get_C_obj_impl_code(cls, obj_type: str, obj_id: int, args: Dict) -> List[str]: + """ + (Per object) this creates the _sendMessage function that other objects use to + send messages to this object. + """ + + lines = super().get_C_obj_impl_code(obj_type, obj_id, args) + + expr = args["expressions"][0] + expr_parser = ExprCWriter(expr) + expr_lines = expr_parser.to_c_simd("bIns", "bOut")[:-1] + expr_lines = "\n".join( + [f"\t{line}" for line in expr_lines] + ) + num_buffers = expr_parser.num_simd_buffers() + buffer_declaration = "\t// no extra buffers needed" + if num_buffers > 0: + buffers = ", ".join([f"Bf{i}" for i in range(0, num_buffers)]) + buffer_declaration = f"\thv_bufferf_t {buffers};" + + func_name = f"Heavy_heavy::{cls.preamble}_{obj_id}_evaluate" + lines.extend([ + "", + f"void {func_name}(hv_bInf_t* bIns, hv_bOutf_t bOut) {{", + + buffer_declaration, + expr_lines, + "}", + + ]) + return lines + + @classmethod + def get_C_process(cls, process_dict: Dict, obj_type: str, obj_id: int, args: Dict) -> List[str]: + input_args = [] + for b in process_dict["inputBuffers"]: + buf = HeavyObject._c_buffer(b) + input_args.append(f"VIf({buf})") + out_buf = HeavyObject._c_buffer(process_dict["outputBuffers"][0]) + out_buf = f"VOf({out_buf})" + + call = [ + "", + "\t// !!! declare this buffer once outside the loop", + f"\thv_bInf_t input_args_{obj_id}[{args['num_inlets']}] = {{{', '.join(input_args)}}};", + f"\t{cls.preamble}_evaluators[{len(cls.obj_eval_functions)}](input_args_{obj_id}, {out_buf});" + "", + ] + + return call diff --git a/hvcc/generators/ir2c/expr_arpeggio_grammar.py b/hvcc/generators/ir2c/expr_arpeggio_grammar.py new file mode 100644 index 00000000..77e1b6e3 --- /dev/null +++ b/hvcc/generators/ir2c/expr_arpeggio_grammar.py @@ -0,0 +1,47 @@ +from arpeggio import ParserPython, StrMatch +from arpeggio import ZeroOrMore +from arpeggio import RegExMatch as regex + + +class SuppressStrMatch(StrMatch): + suppress = True + + +hide = SuppressStrMatch + + +def expr(): return lor # EOF # noqa +def lor(): return land, ZeroOrMore("||", land) # noqa +def land(): return bor, ZeroOrMore("&&", bor) # noqa +def bor(): return xor, ZeroOrMore("|", xor) # noqa +def xor(): return band, ZeroOrMore("^", band) # noqa +def band(): return eq, ZeroOrMore("&", eq) # noqa +def eq(): return gtlt, ZeroOrMore(["==","!="], gtlt) # noqa +def gtlt(): return shift, ZeroOrMore(["<","<=",">",">="], shift) # noqa +def shift(): return term, ZeroOrMore(["<<",">>"], term) # noqa +def term(): return factor, ZeroOrMore(["+","-"], factor) # RtoL # noqa +def factor(): return unary, ZeroOrMore(["*","/","%"], unary) # noqa +def unary(): return [(["-","~","!"], unary), primary] # RtoL # noqa +def primary(): return [number, var, func, group] # noqa +def number(): return [num_f, num_i] # noqa +def var(): return regex(r"\$[fisv]\d+") # noqa +def func(): return [ # noqa + (f_name, hide("("), expr, hide(")")), # noqa + (f_name, hide("("), expr, hide(","), expr, hide(")")), # noqa + (f_name, hide("("), expr, hide(","), expr, hide(","), expr, hide(")")) # noqa + ] # noqa +def group(): return hide("("), expr, hide(")") # noqa +def num_f(): return regex(r"\d+\.\d+") # prob need to cover exponential syntax, others? # noqa +def num_i(): return regex(r"\d+") # need to cover bin/octal/hex? # noqa +def f_name(): return [ # noqa + "abs", "acos", "acosh", "asin", "asinh", "atan", "atan2", # noqa + "cbrt", "ceil", "copysign", "cos", "cosh", "drem", "erf", # noqa + "erfc", "exp", "expm1", "fact", "finite", "float", "floor", # noqa + "fmod", "ldexp", "if", "imodf", "int", "isinf", "isnan", # noqa + "ln", "log", "log10", "log1p", "max", "min", "modf", "pow", # noqa + "rint", "sin", "sinh", "size", "sqrt", "sum", "Sum", # noqa + "tan", "tanh", # noqa + ] # noqa + + +expr_grammar = ParserPython(expr, reduce_tree=True) diff --git a/hvcc/generators/ir2c/expr_arpeggio_parser.py b/hvcc/generators/ir2c/expr_arpeggio_parser.py new file mode 100644 index 00000000..397ccfdb --- /dev/null +++ b/hvcc/generators/ir2c/expr_arpeggio_parser.py @@ -0,0 +1,77 @@ +from .expr_arpeggio_grammar import expr_grammar + + +class ExprArpeggioParser(): + """ + These two methods are the "public" API + """ + @classmethod + def parse(cls, expr): + """Parse the input expression and return a parse tree""" + return expr_grammar.parse(expr) + + @classmethod + def parse_to_ast(cls, expr): + """Parse the input expression and return an AST""" + return cls.to_expr_tree(cls.parse(expr)) + + """ + Helper methods below + """ + @classmethod + def to_expr_tree(cls, expr): + if expr.rule_name == "num_i": + return ExprNode("num_i", expr.value) + elif expr.rule_name == "num_f": + return ExprNode("num_f", expr.value) + elif expr.rule_name == "var": + return ExprNode("var", expr.value) + elif expr.rule_name == "func": + return ExprNode("func", expr[0].value, [cls.to_expr_tree(p) for p in expr[1:]]) + elif expr.rule_name == "unary": + return ExprNode("unary", expr[0].value, [cls.to_expr_tree(expr[1])]) + elif expr.rule_name == "term": + # this is RtoL associativity + val = None + tmp = None + subtree = None + for i in range(len(expr)): + if i % 2 == 0: + val = cls.to_expr_tree(expr[i]) + else: + foo = ExprNode("binary", str(expr[i]), [val]) + if not subtree: + subtree = foo + else: + tmp.nodes.append(foo) + tmp = foo + tmp.nodes.append(val) + return subtree + + elif expr.rule_name in ( + "lor", "land", "bor", "xor", "band", "eq", "gtlt", "shift", "factor" + ): + # this is LtoR associativity + subtree = None + for i in range(len(expr)): + if i % 2 == 0: + if not subtree: + subtree = cls.to_expr_tree(expr[i]) + else: + subtree.nodes.append(cls.to_expr_tree(expr[i])) + else: + subtree = ExprNode("binary", str(expr[i]), [subtree]) + return subtree + + +class ExprNode: + def __init__(self, node_type, value, nodes=[]): + self.type = node_type + self.value = value + self.nodes = nodes + + def __str__(self, depth=0): + this = f"{' '*depth}{self.type}:{self.value}\n" + for node in self.nodes: + this += node.__str__(depth + 1) + return this diff --git a/hvcc/generators/ir2c/expr_c_writer.py b/hvcc/generators/ir2c/expr_c_writer.py new file mode 100644 index 00000000..dde038a1 --- /dev/null +++ b/hvcc/generators/ir2c/expr_c_writer.py @@ -0,0 +1,219 @@ +from .expr_arpeggio_parser import ExprArpeggioParser, ExprNode +import re + + +class ExprCWriter: + def __init__(self, expression): + self.expression = expression + self.expr_tree = ExprArpeggioParser.parse_to_ast(expression) + self.parse_tree = ExprArpeggioParser.parse(expression) + self.num_buffers = None + + def to_ast(self): + return self.expr_tree + + def to_parse_tree(self): + return self.parse_tree + + def to_c_simd(self, vv_in, v_out): + self._simd_bind_variables(vv_in) + self._simd_replace_constants() + return self._to_c_simd(v_out) + + def num_simd_buffers(self): + if self.num_buffers is None: + self.to_c_simd() + return self.num_buffers + + def to_c_nested(self): + return self._to_c_nested() + + def _simd_replace_constants(self): + self._simd_replace_constants_R(self.expr_tree) + + def _simd_replace_constants_R(self, tree): + if tree.type == "func" and tree.value.startswith("_load_"): + return # don't re-replace anything + + for i in range(len(tree.nodes)): + node = tree.nodes[i] + if node.type == "num_i": + tree.nodes[i] = ExprNode("func", "_load_i", [node]) + elif node.type == "num_f": + tree.nodes[i] = ExprNode("func", "_load_f", [node]) + else: + self._simd_replace_constants_R(node) + + def _simd_bind_variables(self, a_name="A"): + self._bind_vars_R(self.expr_tree, a_name) + + def _bind_vars_R(self, tree, a_name): + if tree.type == "var": + self._bind_var_node(tree, a_name) + else: + for node in tree.nodes: + self._bind_vars_R(node, a_name) + + def _bind_var_node(self, node, a_name): + if node.type != "var": + print("called on non-var node: ", node) + return + parts = re.match(r"\$(v|f)(\d+)", node.value) + node.value = f"{a_name}[{int(parts[2])-1}]" + node.type = "bound_var" + + class BufferAllocator: + """Inner class for managing the swapping of buffers from + output to input in successive calls. + """ + def __init__(self): + self._avail = set() + self._next = 0 + + def next(self): + """If a buffer is available return it, otherwise + allocate a new one and return it. """ + + if len(self._avail) > 0: + return self._avail.pop() + nxt = self._next + self._next += 1 + return nxt + + def free(self, n): + """Return a buffer back to the pool to be reused""" + self._avail.add(n) + + def num_allocated(self): + """Return the buffers allocated in so far.""" + return self._next + + def _to_c_simd(self, v_out): + ba = ExprCWriter.BufferAllocator() + lines = [] + + def _to_c_simd_R(expr_tree, r_vec=None): + if expr_tree.type in ("num_i", "num_f", "var", "bound_var"): + return expr_tree.value + + args = [] + buffers = [] + + if ( + expr_tree.type == "func" + and expr_tree.value.startswith("_load_") + ): + next_buf = f"Bf{ba.next()}" + args.append("&" + next_buf) + const_value = _to_c_simd_R(expr_tree.nodes[0]) + for i in range(8): + args.append(const_value) + else: + for node in expr_tree.nodes: + val = _to_c_simd_R(node) + args.append(val) + if type(val) == str and val.startswith("Bf"): + buffers.append(val) + if r_vec: + next_buf = r_vec + args.append(next_buf) + else: + next_buf = f"Bf{ba.next()}" + args.append("&" + next_buf) + + f_name = ExprOpMap.get_hv_func_simd(expr_tree.value) + lines.append(f"{f_name}({', '.join(args)});") + [ba.free(int(b[2])) for b in buffers] + + return next_buf + + lines.append(_to_c_simd_R(self.expr_tree, v_out)) + self.num_buffers = ba.num_allocated() + return lines + + def _to_c_nested(self): + """Output C-code as nested function calls""" + + def _to_c_nested_R(expr_tree): + if expr_tree.type in ("num_i", "num_f", "var"): + return expr_tree.value + else: + f_name = ExprOpMap.get_hv_func(expr_tree.value) + args = [_to_c_nested_R(p) for p in expr_tree.nodes] + return f"{f_name}({', '.join(args)})" + + return _to_c_nested_R(self.expr_tree) + ";" + + +class ExprOpMap: + op_map = { + "~": "hv_?_f", + # "-": "hv_neg_f", + "*": "hv_mul_f", + "/": "hv_div_f", + "%": "hv_?_f", + "+": "hv_add_f", + "-": "hv_sub_f", + "<": "hv_lt_f", + "<=": "hv_lte_f", + ">": "hv_gt_f", + ">=": "hv_gte_f", + "!=": "hv_neq_f", + "&&": "hv_and_f", + "||": "hv_or_f", + "abs": "hv_abs_f", + "acos": "hv_acos_f", + "acosh": "hv_acosh_f", + "asin": "hv_asin_f", + "asinh": "hv_asinh_f", + "atan": "hv_atan_f", + "atan2": "hv_atan2_f", + "cbrt": "hv_?_f", + "ceil": "hv_ceil_f", + "copysign": "hv_?_f", # does this just return +/- 1? It doesn't come up in pd... + "cos": "hv_cos_f", + "cosh": "hv_cosh_f", + "drem": "hv_?_f", + "erf": "hv_?_f", + "erfc": "hv_?_f", + "exp": "hv_exp_f", + "expm1": "hv_?_f", + "fact": "hv_?_f", + "finite": "hv_?_f", + "float": "hv_cast_if", + "floor": "hv_floor_f", + "fmod": "hv_?_f", + "ldexp": "hv_?_f", + "if": "hv_?_f", + "imodf": "hv_?_f", + "int": "hv_cast_fi", + "isinf": "hv_?_f", + "isnan": "hv_?_f", + "ln": "hv_?_f", + "log": "hv_?_f", + "log10": "hv_?_f", + "log1p": "hv_?_f", + "max": "hv_max_f", + "min": "hv_min_f", + "modf": "hv_?_f", + "pow": "hv_pow_f", + "rint": "hv_?_f", # round to nearest int + "sin": "hv_sin_f", + "sinh": "hv_sinh_f", + "size": "hv_?_f", + "sqrt": "hv_sqrt_f", + "sum": "hv_?_f", # sum of all elements of a table + "Sum": "hv_?_f", # sum of elemnets of a specified boundary of a table??? + "tan": "hv_tan_f", + "tanh": "hv_tanh_f", + "_load_f": "hv_var_k_f", + "_load_i": "hv_var_k_i", + } + + @classmethod + def get_hv_func(cls, symbol): + return cls.op_map[symbol] + + @classmethod + def get_hv_func_simd(cls, symbol): + return "__" + cls.op_map[symbol] diff --git a/hvcc/generators/ir2c/ir2c.py b/hvcc/generators/ir2c/ir2c.py index c822c007..0363c923 100644 --- a/hvcc/generators/ir2c/ir2c.py +++ b/hvcc/generators/ir2c/ir2c.py @@ -31,6 +31,7 @@ from hvcc.generators.ir2c.ControlBinop import ControlBinop from hvcc.generators.ir2c.ControlCast import ControlCast from hvcc.generators.ir2c.ControlDelay import ControlDelay +from hvcc.generators.ir2c.ControlExpr import ControlExpr from hvcc.generators.ir2c.ControlIf import ControlIf from hvcc.generators.ir2c.ControlMessage import ControlMessage from hvcc.generators.ir2c.ControlPack import ControlPack @@ -53,6 +54,7 @@ from hvcc.generators.ir2c.SignalCPole import SignalCPole from hvcc.generators.ir2c.SignalDel1 import SignalDel1 from hvcc.generators.ir2c.SignalEnvelope import SignalEnvelope +from hvcc.generators.ir2c.SignalExpr import SignalExpr from hvcc.generators.ir2c.SignalLine import SignalLine from hvcc.generators.ir2c.SignalLorenz import SignalLorenz from hvcc.generators.ir2c.SignalMath import SignalMath @@ -78,6 +80,8 @@ class ir2c: "__cast_b": ControlCast, "__cast_f": ControlCast, "__cast_s": ControlCast, + "__expr": ControlExpr, + "__expr~": SignalExpr, "__message": ControlMessage, "__system": ControlSystem, "__receive": ControlReceive, @@ -197,8 +201,13 @@ def compile( free_list = [] def_list = [] decl_list = [] + obj_header_lines = [] + obj_impl_lines = [] + class_header_lines = [] + class_impl_lines = [] for obj_id in ir["init"]["order"]: o = ir["objects"][obj_id] + print("init objects:", o["type"]) obj_class = ir2c.get_class(o["type"]) init_list.extend(obj_class.get_C_init(o["type"], obj_id, o["args"])) def_list.extend(obj_class.get_C_def(o["type"], obj_id)) @@ -208,13 +217,15 @@ def compile( for x in ir["control"]["sendMessage"]: obj_id = x["id"] o = ir["objects"][obj_id] + print("control objects:", o["type"]) obj_class = ir2c.get_class(o["type"]) impl = obj_class.get_C_impl( o["type"], obj_id, x["onMessage"], ir2c.get_class, - ir["objects"]) + ir["objects"], + o["args"]) impl_list.append("\n".join(PrettyfyC.prettyfy_list(impl))) decl_list.extend(obj_class.get_C_decl(o["type"], obj_id, o["args"])) @@ -230,15 +241,36 @@ def compile( # generate the list of functions to process process_list: List = [] + # print("--------------- for each signal in order") + process_classes = set() for x in ir["signal"]["processOrder"]: + # print("--- signal", x["id"], o["type"], ir2c.get_class(o["type"])) obj_id = x["id"] o = ir["objects"][obj_id] - process_list.extend(ir2c.get_class(o["type"]).get_C_process( + print("process objects:", o["type"]) + obj_cls = ir2c.get_class(o["type"]) + process_classes.add(obj_cls) + process_list.extend(obj_cls.get_C_process( x, o["type"], obj_id, o["args"])) + # begin experiment for expr~ + obj_header_lines.extend(obj_cls.get_C_obj_header_code( + o["type"], obj_id, o["args"] + )) + obj_impl_lines.extend(obj_cls.get_C_obj_impl_code( + o["type"], obj_id, o["args"] + )) + # once for each class + for prc_cls in process_classes: + class_header_lines.extend(prc_cls.get_C_class_header_code( + o["type"], o["args"] + )) + class_impl_lines.extend(prc_cls.get_C_class_impl_code( + o["type"], o["args"] + )) # # Load the C-language template files and use the parsed strings to fill them in. # @@ -262,7 +294,9 @@ def compile( def_list=def_list, signal=ir["signal"], copyright=copyright, - externs=externs)) + externs=externs, + class_header_lines=class_header_lines, + obj_header_lines=obj_header_lines)) # write C++ implementation with open(os.path.join(output_dir, f"Heavy_{name}.cpp"), "w") as f: @@ -276,7 +310,9 @@ def compile( send_table=ir["tables"], process_list=process_list, table_data_list=table_data_list, - copyright=copyright)) + copyright=copyright, + class_impl_lines=class_impl_lines, + obj_impl_lines=obj_impl_lines)) # write C API, hv_NAME.h with open(os.path.join(output_dir, f"Heavy_{name}.h"), "w") as f: diff --git a/hvcc/generators/ir2c/static/HvControlExpr.c b/hvcc/generators/ir2c/static/HvControlExpr.c new file mode 100644 index 00000000..b15f05f9 --- /dev/null +++ b/hvcc/generators/ir2c/static/HvControlExpr.c @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2014-2018 Enzien Audio Ltd. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include "HvControlExpr.h" + +hv_size_t cExpr_init(ControlExpr *o, float(*eval_fptr)(float*)) { + o->eval_fptr = eval_fptr; + for(int i=0; i < MAX_EXPR_ARGS; i++) { + o->args[i] = 0.0f; + } + return 0; +} + +void cExpr_free(ControlExpr *o) { + ; +} + + +void cExpr_onMessage(HeavyContextInterface *_c, ControlExpr *o, int letIn, const HvMessage *m, + void (*sendMessage)(HeavyContextInterface *, int, const HvMessage *)) { + + int numElements = msg_getNumElements(m); + switch (letIn) { + case 0: { // first inlet stores all values of input msg and triggers an output + if (msg_isBang(m,0)) { + ; // pass through to sending the msg below + + } else if (! msg_isFloat(m,0)) { + printf("Got value other than bang or float\n"); + break; + + } else { + for (int i = hv_min_i(numElements, msg_getNumElements(m))-1; i >= 0; --i) { + if (msg_isFloat(m, i)) { + o->args[i] = msg_getFloat(m, i); + } else { + printf("Got value other than float\n"); + } + } + } + + // send result of expression + HvMessage *n = HV_MESSAGE_ON_STACK(1); + float f = o->eval_fptr(o->args); + msg_initWithFloat(n, msg_getTimestamp(m), f); + sendMessage(_c, 0, n); + break; + } + default: { // rest of inlets just store values + if (msg_isFloat(m,0)) { + o->args[letIn] = msg_getFloat(m, 0); + } else { + printf("Got value other than float\n"); + } + break; + } + } +} diff --git a/hvcc/generators/ir2c/static/HvControlExpr.h b/hvcc/generators/ir2c/static/HvControlExpr.h new file mode 100644 index 00000000..7c062f33 --- /dev/null +++ b/hvcc/generators/ir2c/static/HvControlExpr.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2014-2018 Enzien Audio Ltd. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _HEAVY_CONTROL_EXPR_H_ +#define _HEAVY_CONTROL_EXPR_H_ + +#include "HvHeavyInternal.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MAX_EXPR_ARGS 10 + +typedef struct ControlExpr { + float args[MAX_EXPR_ARGS]; + float(*eval_fptr)(float*); +} ControlExpr; + +hv_size_t cExpr_init(ControlExpr *o, float(*eval_fptr)(float*)); + +void cExpr_free(ControlExpr *o); + +void cExpr_onMessage(HeavyContextInterface *_c, ControlExpr *o, int letIn, const HvMessage *m, + void (*sendMessage)(HeavyContextInterface *, int, const HvMessage *)); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // _HEAVY_CONTROL_EXPR_H_ diff --git a/hvcc/generators/ir2c/static/HvControlPack.c b/hvcc/generators/ir2c/static/HvControlPack.c index 7ae73430..edfa575c 100644 --- a/hvcc/generators/ir2c/static/HvControlPack.c +++ b/hvcc/generators/ir2c/static/HvControlPack.c @@ -22,14 +22,15 @@ hv_size_t cPack_init(ControlPack *o, int nargs, ...) { hv_assert(o->msg != NULL); msg_init(o->msg, nargs, 0); - // variable arguments are used as float initialisers for the pack elements + // variable arguments are used as float initialisers for the Expr elements va_list ap; va_start(ap, nargs); for (int i = 0; i < nargs; ++i) { msg_setFloat(o->msg, i, (float) va_arg(ap, double)); } va_end(ap); - return numBytes; + return numBytes; + } void cPack_free(ControlPack *o) { diff --git a/hvcc/generators/ir2c/templates/Heavy_NAME.cpp b/hvcc/generators/ir2c/templates/Heavy_NAME.cpp index a9e0a33c..c7d8a8fc 100644 --- a/hvcc/generators/ir2c/templates/Heavy_NAME.cpp +++ b/hvcc/generators/ir2c/templates/Heavy_NAME.cpp @@ -155,6 +155,21 @@ void Heavy_{{name}}::{{x}} {%- endfor %} +/* + * Experimental code for expr/expr~ implementation + * Write out the generic header code + */ + + // per class code + {%- for line in class_impl_lines %} + {{line}} + {%- endfor %} + + // per object code + {%- for line in obj_impl_lines %} + {{line}} + {%- endfor %} + /* * Context Process Implementation diff --git a/hvcc/generators/ir2c/templates/Heavy_NAME.hpp b/hvcc/generators/ir2c/templates/Heavy_NAME.hpp index 75777a7c..d03df5a1 100644 --- a/hvcc/generators/ir2c/templates/Heavy_NAME.hpp +++ b/hvcc/generators/ir2c/templates/Heavy_NAME.hpp @@ -85,6 +85,23 @@ class Heavy_{{name}} : public HeavyContext { HvTable *getTableForHash(hv_uint32_t tableHash) override; void scheduleMessageForReceiver(hv_uint32_t receiverHash, HvMessage *m) override; + + /* + * Experimental code for expr/expr~ implementation + * Write out the generic header code + */ + + // per class code + {%- for line in class_header_lines %} + {{line}} + {%- endfor %} + + // per object code + {%- for line in obj_header_lines %} + {{line}} + {%- endfor %} + + // static sendMessage functions {%- for d in decl_list %} static void {{d}} diff --git a/hvcc/interpreters/pd2hv/PdExprObject.py b/hvcc/interpreters/pd2hv/PdExprObject.py new file mode 100644 index 00000000..f449597a --- /dev/null +++ b/hvcc/interpreters/pd2hv/PdExprObject.py @@ -0,0 +1,90 @@ +# Copyright (C) 2014-2018 Enzien Audio, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import re + +from .PdObject import PdObject + +from typing import Optional, List + + +class PdExprObject(PdObject): + """ + Limitations (compared to vanilla pd): + - only supports a single expression + - pd docs say expr support 9 variables, experiments show that + it supports at least 20... This version currently supports up + to 10 variables as defined in HvControlExpr.h + - I don't know what pd-expr does with strings, haven't experimented + and haven't given it any thought yet here + + Bugs: + - skipped variables cause crash, like "$f1 + $f3" + """ + + def __init__( + self, + obj_type: str, + obj_args: Optional[List] = None, + pos_x: int = 0, + pos_y: int = 0 + ) -> None: + """ + Validate the expr object and any heavy restrictions, then + convert it directly into a HeavyIR object. + """ + # self.obj_type = obj_type + + print("In Pd expr Obj") + assert obj_type in ["expr", "expr~"] + super().__init__(obj_type, obj_args, pos_x, pos_y) + + # turn the arguments into a list of expressions, but only one for now + if len(self.obj_args) == 0: + self.add_error("Empty expression") + + expressions = [e.strip() for e in " ".join(self.obj_args).split("\\;")] + if len(expressions) > 1: + self.add_error("Heavy expr does not support multiple expressions") + else: + self.expressions = expressions + + # count the number of inlets + var_nums = { + int(var[2:]) for var in + # this should be checked separately for control vs. signal instances + # fis for control, v for signal + re.findall(r"\$[fisv]\d+", self.expressions[0]) + } + self.num_inlets = max(var_nums) if len(var_nums) > 0 else 1 + if self.num_inlets > 10: + self.add_error("Heavy expr supports upto 10 variables") + + def validate_configuration(self): + # things that could be validated: + # - inlet count/types match variables in the expression(s) + pass + + def to_hv(self): + return { + "type": f"__{self.obj_type}", + "args": { + "expressions": self.expressions, + "num_inlets": self.num_inlets + }, + "properties": { + "x": self.pos_x, + "y": self.pos_y + } + } diff --git a/hvcc/interpreters/pd2hv/PdParser.py b/hvcc/interpreters/pd2hv/PdParser.py index f0818de4..2647a64a 100644 --- a/hvcc/interpreters/pd2hv/PdParser.py +++ b/hvcc/interpreters/pd2hv/PdParser.py @@ -26,6 +26,7 @@ from .HvSwitchcase import HvSwitchcase # __switchcase from .PdAudioIoObject import PdAudioIoObject # adc~/dac~ from .PdBinopObject import PdBinopObject # binary arithmatic operators +from .PdExprObject import PdExprObject # expr/expr~ from .PdGraph import PdGraph # canvas from .PdLetObject import PdLetObject # inlet/inlet~/outlet/outlet~ from .PdMessageObject import PdMessageObject # msg @@ -344,6 +345,7 @@ def graph_from_canvas( # do we have an abstraction for this object? abs_path = self.find_abstraction_path(os.path.dirname(pd_path), obj_type) if abs_path is not None and not g.is_abstraction_on_call_stack(abs_path): + # print("PdParser: find object - abstraction") # ensure that infinite recursion into abstractions is not possible x = self.graph_from_file( file_path=abs_path, @@ -353,6 +355,7 @@ def graph_from_canvas( # is this object in lib/pd_converted? elif os.path.isfile(os.path.join(self.__PDLIB_CONVERTED_DIR, f"{obj_type}.hv.json")): + # print("PdParser: find object - pd_converted") self.obj_counter[obj_type] += 1 hv_path = os.path.join(self.__PDLIB_CONVERTED_DIR, f"{obj_type}.hv.json") x = HeavyGraph( @@ -362,6 +365,7 @@ def graph_from_canvas( # is this object in lib/heavy_converted? elif os.path.isfile(os.path.join(self.__HVLIB_CONVERTED_DIR, f"{obj_type}.hv.json")): + # print("PdParser: find object - heavy_converted") self.obj_counter[obj_type] += 1 hv_path = os.path.join(self.__HVLIB_CONVERTED_DIR, f"{obj_type}.hv.json") x = HeavyGraph( @@ -371,6 +375,7 @@ def graph_from_canvas( # is this object in lib/pd? elif os.path.isfile(os.path.join(self.__PDLIB_DIR, f"{obj_type}.pd")): + # print("PdParser: find object - pd") self.obj_counter[obj_type] += 1 pdlib_path = os.path.join(self.__PDLIB_DIR, f"{obj_type}.pd") @@ -411,6 +416,7 @@ def graph_from_canvas( # is this object in lib/heavy? elif os.path.isfile(os.path.join(self.__HVLIB_DIR, f"{obj_type}.pd")): + # print("PdParser: find object - heavy") self.obj_counter[obj_type] += 1 hvlib_path = os.path.join(self.__HVLIB_DIR, f"{obj_type}.pd") x = self.graph_from_file( @@ -441,6 +447,7 @@ def graph_from_canvas( # is this an object that must be programatically parsed? elif obj_type in self.__PD_CLASSES: + # print("PdParser: find object - prog-parse") self.obj_counter[obj_type] += 1 obj_class = self.__PD_CLASSES[obj_type] x = obj_class( @@ -449,6 +456,7 @@ def graph_from_canvas( pos_x=int(line[2]), pos_y=int(line[3])) elif self.__is_float(obj_type): + # print("PdParser: find object - float") # parse float literals self.obj_counter["float"] += 1 x = HeavyObject( @@ -648,6 +656,8 @@ def __resolve_object_args( __PD_CLASSES = { "adc~": PdAudioIoObject, "dac~": PdAudioIoObject, + "expr": PdExprObject, + "expr~": PdExprObject, "inlet": PdLetObject, "inlet~": PdLetObject, "outlet": PdLetObject, diff --git a/setup.cfg b/setup.cfg index 6b8e2844..4c77be9b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,7 @@ include_package_data = True packages = find: python_requires = >= 3.8 install_requires = + Arpeggio>=2.0.0 Jinja2>=2.11 importlib_resources>=5.1 wstd2daisy>=0.5.2 diff --git a/tests/pd/control_expr/test-div.golden.txt b/tests/pd/control_expr/test-div.golden.txt new file mode 100644 index 00000000..659e01a3 --- /dev/null +++ b/tests/pd/control_expr/test-div.golden.txt @@ -0,0 +1 @@ +[@ 0.000] print: 0.148387 diff --git a/tests/pd/control_expr/test-div.pd b/tests/pd/control_expr/test-div.pd new file mode 100644 index 00000000..a6ff55a3 --- /dev/null +++ b/tests/pd/control_expr/test-div.pd @@ -0,0 +1,16 @@ +#N canvas 670 156 450 300 12; +#X obj 55 17 loadbang; +#X obj 55 152 print; +#X msg 116 77 3.1; +#X msg 55 77 2.3; +#X obj 55 42 t b b b; +#X msg 178 77 5.5; +#X obj 55 119 expr ($f1/$f2)/$i3; +#X connect 0 0 4 0; +#X connect 2 0 6 1; +#X connect 3 0 6 0; +#X connect 4 0 3 0; +#X connect 4 1 2 0; +#X connect 4 2 5 0; +#X connect 5 0 6 2; +#X connect 6 0 1 0; diff --git a/tests/pd/control_expr/test-mult.golden.txt b/tests/pd/control_expr/test-mult.golden.txt new file mode 100644 index 00000000..46f3126d --- /dev/null +++ b/tests/pd/control_expr/test-mult.golden.txt @@ -0,0 +1 @@ +[@ 0.000] print: 35.65 diff --git a/tests/pd/control_expr/test-mult.pd b/tests/pd/control_expr/test-mult.pd new file mode 100644 index 00000000..40e521c5 --- /dev/null +++ b/tests/pd/control_expr/test-mult.pd @@ -0,0 +1,16 @@ +#N canvas 670 156 450 300 12; +#X obj 55 17 loadbang; +#X obj 55 152 print; +#X msg 109 77 3.1; +#X msg 55 77 2.3; +#X obj 55 119 expr $f1*$f2*$i3; +#X obj 55 42 t b b b; +#X msg 164 77 5.5; +#X connect 0 0 5 0; +#X connect 2 0 4 1; +#X connect 3 0 4 0; +#X connect 4 0 1 0; +#X connect 5 0 3 0; +#X connect 5 1 2 0; +#X connect 5 2 6 0; +#X connect 6 0 4 2; diff --git a/tests/test_control_expr.py b/tests/test_control_expr.py new file mode 100755 index 00000000..c38a4eb7 --- /dev/null +++ b/tests/test_control_expr.py @@ -0,0 +1,50 @@ +# Copyright (C) 2014-2018 Enzien Audio, Ltd. +# Copyright (C) 2022 Wasted Audio +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import os + +from tests.framework.base_control import TestPdControlBase + + +class TestPdControlExprPatches(TestPdControlBase): + SCRIPT_DIR = os.path.dirname(__file__) + TEST_DIR = os.path.join(os.path.dirname(__file__), "pd", "control_expr") + + def test_mult(self): + self._test_control_patch("test-mult.pd") + + def test_div(self): + self._test_control_patch("test-div.pd") + + +def main(): + # TODO(mhroth): make this work + parser = argparse.ArgumentParser( + description="Compile a specific pd patch.") + parser.add_argument( + "pd_path", + help="The path to the Pd file to read.") + args = parser.parse_args() + if os.path.exists(args.pd_path): + result = TestPdControlExprPatches._test_control_patch(args.pd_path) + print(result) + else: + print(f"Pd file path '{args.pd_path}' doesn't exist") + + +if __name__ == "__main__": + main() diff --git a/tox.ini b/tox.ini index 812a253b..bd892768 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,7 @@ deps = basepython = python3 commands = - mypy hvcc + mypy --ignore-missing-imports hvcc [run] ignore = examples/* @@ -44,4 +44,5 @@ omit = .tox/*,venv/*,tests/*,examples/*,setup.py [flake8] max-line-length = 120 -exclude = .tox,venv,build,examples +exclude = + .tox,venv,build,examples