diff --git a/maxdiff/frozen_device_printer.py b/maxdiff/frozen_device_printer.py index 5ebd9c4..684b02a 100644 --- a/maxdiff/frozen_device_printer.py +++ b/maxdiff/frozen_device_printer.py @@ -1,4 +1,5 @@ from freezing_utils import * +from get_frozen_stats import get_frozen_stats def print_frozen_device(data: bytes) -> str: @@ -20,6 +21,7 @@ def print_frozen_device(data: bytes) -> str: device_entries = get_device_entries(data, footer_entries) for entry in device_entries: frozen_string += entry["description"] + "\n" + frozen_string += get_frozen_stats(device_entries) return frozen_string diff --git a/maxdiff/get_frozen_stats.py b/maxdiff/get_frozen_stats.py new file mode 100644 index 0000000..6d72cb8 --- /dev/null +++ b/maxdiff/get_frozen_stats.py @@ -0,0 +1,125 @@ +import json +from freezing_utils import footer_entry_with_data + + +def get_frozen_stats(entries: list[footer_entry_with_data]): + """Returns statistics for this device""" + + device_data = entries[0]["data"] # the first entry is always the device file + + abstractions = [item for item in entries if item["file_name"].endswith(".maxpat")] + + # cache names of known abstractions + file_names = [item["file_name"] for item in abstractions] + + patch = get_patcher_dict(device_data) + object_count = count_objects(patch, abstractions, file_names) + + summary = "\n" + summary += "Total - Counting every abstraction instance - Indicates loading time\n" + summary += f" Object instances: {object_count}\n" + summary += " Connections:\n" + summary += "Unique - Counting abstractions once - Indicates maintainability\n" + summary += " Object instances:\n" + summary += " Connections:\n" + return summary + + +def count_objects(patcher, entries: list[dict], file_names: list[str]): + """Recursively counts all object instances in this patcher, + inluding in every instance of its dependencies""" + boxes = patcher["boxes"] + count = 0 + for box_entry in boxes: + box = box_entry["box"] + count += 1 + + if "patcher" in box: + patch = box["patcher"] + if box.get("maxclass") == "bpatcher" and box.get("embed") == 1: + # get embedded bpatcher count + count += count_objects(patch, entries, file_names) + else: + # get subpatcher count + count += count_objects(patch, entries, file_names) + else: + abstraction_name = get_abstraction_name(box, file_names) + if abstraction_name is None: + continue + + abstraction = [ + item for item in entries if item["file_name"] == abstraction_name + ][0] + abstraction_data = abstraction["data"] + abstraction_patch = get_patcher_dict(abstraction_data) + abstraction_count = count_objects(abstraction_patch, entries, file_names) + + if "text" in box and box["text"].startswith("poly~"): + # get poly abstraction count + voice_count = int(box["text"].split(" ")[2]) + count += abstraction_count * voice_count + else: + # get abstraction count + count += abstraction_count + + return count + + +def get_abstraction_name(box, file_names: list[str]): + """ + Checks if this box is an abstraction and if so, return the name of the abstraction file. + - returns None if this is not an abstraction + - throws error if an abstraction name was expected but it was not found in the list of known names + """ + if "text" in box: + if box["text"].startswith("poly~"): + name = box["text"].split(" ")[1] + ".maxpat" + if name in file_names: + return name + else: + raise ValueError( + "poly~ pointing to file that is not known as a dependency: " + name + ) + else: + name = box["text"].split(" ")[0] + ".maxpat" + if name in file_names: + return name + + if box.get("maxclass") == "bpatcher" and box.get("embed") != 1: + if box.get("name") in file_names: + return box["name"] + else: + raise ValueError( + "Non-embedded bpatcher pointing to file that is not known as a dependency: " + + box["name"] + ) + + return None + + +def get_patcher_dict(patch_data): + """Returns the dict that is represents the given patcher data. + Throws errors if parsing fails""" + device_data_text = "" + + try: + if patch_data[len(patch_data) - 1] == 0: + device_data_text = patch_data[: len(patch_data) - 1].decode("utf-8") + else: + device_data_text = patch_data.decode("utf-8") + except Exception as e: + print(f"Error getting device patch data as text: {e}") + + if device_data_text == "": + print("Device has no data") + + patcher_dict = {} + try: + patcher_dict = json.loads(device_data_text) + except ValueError as e: + print(f"Error parsing device patch data as json: {e}") + + if not "patcher" in patcher_dict: + print("Device content does not seem to be a patcher") + + return patcher_dict["patcher"] diff --git a/maxdiff/tests/test.py b/maxdiff/tests/test.py index 99cd4ca..6b4b3fd 100644 --- a/maxdiff/tests/test.py +++ b/maxdiff/tests/test.py @@ -40,6 +40,16 @@ def test_parse_frozen_device(self): actual = parse(test_path) self.assertEqual(expected, actual) + def test_parse_frozen_device(self): + self.maxDiff = None + + expected_path, test_path = get_test_path_files("StatsTest.amxd") + + with open(expected_path, mode="r") as expected_file: + expected = expected_file.read() + actual = parse(test_path) + self.assertEqual(expected, actual) + def test_parse_maxpat(self): self.maxDiff = None diff --git a/maxdiff/tests/test_baselines/FrozenTest.amxd.txt b/maxdiff/tests/test_baselines/FrozenTest.amxd.txt index 3536011..8cac611 100644 --- a/maxdiff/tests/test_baselines/FrozenTest.amxd.txt +++ b/maxdiff/tests/test_baselines/FrozenTest.amxd.txt @@ -10,3 +10,10 @@ hz-icon.svg: 484 bytes, modified at 2024/05/24 13:59:36 UTC beat-icon.svg: 533 bytes, modified at 2024/05/24 13:59:36 UTC fpic.png: 7094 bytes, modified at 2024/05/24 13:59:36 UTC collContent.txt: 8 bytes, modified at 2024/05/24 13:59:36 UTC + +Total - Counting every abstraction instance - Indicates loading time + Object instances: 44 + Connections: +Unique - Counting abstractions once - Indicates maintainability + Object instances: + Connections: diff --git a/maxdiff/tests/test_baselines/StatsTest.amxd.txt b/maxdiff/tests/test_baselines/StatsTest.amxd.txt new file mode 100644 index 0000000..b8dfa6e --- /dev/null +++ b/maxdiff/tests/test_baselines/StatsTest.amxd.txt @@ -0,0 +1,13 @@ +Audio Effect Device +------------------- +Device is frozen +----- Contents ----- +SummaryTest.amxd: 6376 bytes, modified at 2024/06/27 07:39:44 UTC +MyAbstraction.maxpat: 2015 bytes, modified at 2024/06/24 07:11:15 UTC + +Total - Counting every abstraction instance - Indicates loading time + Object instances: 27 + Connections: +Unique - Counting abstractions once - Indicates maintainability + Object instances: + Connections: diff --git a/maxdiff/tests/test_files/MyAbstraction.maxpat b/maxdiff/tests/test_files/MyAbstraction.maxpat index 283c322..9bb3c77 100644 --- a/maxdiff/tests/test_files/MyAbstraction.maxpat +++ b/maxdiff/tests/test_files/MyAbstraction.maxpat @@ -3,8 +3,8 @@ "fileversion" : 1, "appversion" : { "major" : 8, - "minor" : 5, - "revision" : 5, + "minor" : 6, + "revision" : 2, "architecture" : "x64", "modernui" : 1 } @@ -39,6 +39,18 @@ "subpatcher_template" : "", "assistshowspatchername" : 0, "boxes" : [ { + "box" : { + "id" : "obj-3", + "maxclass" : "button", + "numinlets" : 1, + "numoutlets" : 1, + "outlettype" : [ "bang" ], + "parameter_enable" : 0, + "patching_rect" : [ 113.0, 60.0, 24.0, 24.0 ] + } + + } +, { "box" : { "comment" : "", "id" : "obj-2", @@ -67,6 +79,15 @@ "lines" : [ { "patchline" : { "destination" : [ "obj-2", 0 ], + "order" : 1, + "source" : [ "obj-1", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-3", 0 ], + "order" : 0, "source" : [ "obj-1", 0 ] } diff --git a/maxdiff/tests/test_files/StatsTest.amxd b/maxdiff/tests/test_files/StatsTest.amxd new file mode 100644 index 0000000..9b28f11 Binary files /dev/null and b/maxdiff/tests/test_files/StatsTest.amxd differ diff --git a/maxdiff/tests/test_rewrite_baselines.py b/maxdiff/tests/test_rewrite_baselines.py index 390cac9..dbcc68f 100644 --- a/maxdiff/tests/test_rewrite_baselines.py +++ b/maxdiff/tests/test_rewrite_baselines.py @@ -14,6 +14,7 @@ def run(): rewrite_file("Test.amxd") rewrite_file("EncryptedTest.amxd") rewrite_file("FrozenTest.amxd") + rewrite_file("StatsTest.amxd") rewrite_file("Test.maxpat") rewrite_file("Test Project/Zipped.als") rewrite_file("Test Project/Test.als")