[
  {
    "path": "Examples/patch1.txt",
    "content": "VOICE 1:\n\t- Metropolis (Pitch) p> Braids (1voct) [weight=3]\n\t- Metropolis (Gate) g> Function (Trigger)\n\t- Braids (Out) -> Optomix (Ch1 Signal)\n\t- Function (PosOut) >> Optomix (Ch1 CV)\n\t- Function (NegOut) >> Braids (Timbre CV)\n\t* Metropolis:\n\t| BPM = 124\n\t| Swing = 0\n\t| Root = F\n\t| Scale = Minor\n\t| Mode = F. Forward\n\t| Stages = 16\n\t\n\t* Braids:\n\t| Mode = Fold\n\t| Timbre = 30%\n\t| Timbre CV = -20%\n\t| Color = 0%\n\n\t* Function: Rise = 50% | Fall = 50% | Curve = 30%\n\t* Optomix: Damp = 0% | Control = 100%\n"
  },
  {
    "path": "Examples/patch2.txt",
    "content": "VOICE 1:\n\t- Metropolis (Pitch) p> Lizard2 (CV)\n\t- Lizard2 (Out 2) -> Multifilter (Input)\n\t- Multifilter (LPF) -> Optomix (Ch1 Signal)\n\t- Metropolis (Pitch) p> Multifilter (1V/oct)\n\t- Metropolis (Gate) g> Z4000 (Gate)\n\t- Z4000 (Out) >> Optomix (Ch1 CV)\n\t- Metropolis (Gate) g> Function (Gate)\n\t- Function (Pos Out) >> Multifilter (CV Input)\n\nVOICE 2:\n\t- Metropolis (Clock) c> SequentialVoltage (CLK In)\n\t- Metropolis (Reset) t> SequentialVoltage (Reset)\n\t- SequentialVoltage (Out) >> Braids (1V/Oct)\n\t- Braids (Out) -> A124 (Input)\n\t- e350 (XY) >> A124 (CV2)\n\t- MultiLFO (LFO 2) >> Braids (Timbre)"
  },
  {
    "path": "Examples/syncpll.txt",
    "content": "VOICE 1:\n\n\t- Metropolis (Pitch) p> Aether VCO (CV)\n\t- Metropolis (Gate) g> Maths (Ch 1 Trigger)\n\t- Metropolis (Gate) g> Maths (Ch 4 Trigger)\n\t\n\t* Aether VCO: LFO Freq = 5 | LFO PWM = 7\n\t- Aether VCO (Pulse) -> Mixer (Ch1)\n\t- Aether VCO (Sub 1) -> Tides (Clk)\n\t- Tides (Bi) -> Mixer (Ch2)\n\t- Aether VCO (Sub 2) -> Z3000 (HSync)\n\t- Z3000 (Saw) -> Mixer (Ch3)\n\t\n\t- MultiLFO (LFO 1) >> Tides (Smoothness)\n\t- MultiLFO (LFO 2 Triangle) >> Tides (Shape)\n\t- MultiLFO (LFO 3 Triangle) >> Z3000 (PWM)\n\t* MultiLFO:\n\t| LFO 1 Freq = 3.8\n\t| LFO 1 Shape = Sine\n\t| LFO 1 S&H = 0\n\t| LFO 2 Freq = 1\n\t| LFO 3 Freq = 1\n\t* Tides: PLL Mode = True | Freq = 60% | Smoothness = 70%\n\t* Z3000: Freq = 1pm\n\t\n\t- Maths (Ch 1) >> Multifilter (CV)\n\t- Maths (Ch 4) >> uVCA (Ch1 CV)\n\t\n\t- Mixer (Output) -> Multifilter (Input)\n\t- Multifilter (LPF) -> uVCA (Ch1 Input)\n\t- uVCA (Ch1 Output) -> AUDIO INTERFACE (In 3)\n\n\t"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Spektro Audio\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![Patchbook Logo](/Images/patchbook-logo.jpg)\n\n# About PatchBook:\n\nPatchBook is a markup language and parser for writing and distributing patches for modular synthesizers created by [Spektro Audio](http://spektroaudio.com/).\nThe markup language is designed to be easily readable and writeable by humans while the parser can process .txt files written in the PatchBook format and output a JSON file that can be used by other applications to display and process the patch's data.\n\n**Patchbook Version:** 1.2\n**Parser version:** b3\n\n**Table of Content:**\n\n<!-- MarkdownTOC -->\n\n- Markup Description\n\t- Voices\n\t- Connections\n\t- Parameters\n\t- Comments:\n\t- Examples\n- Parser\n\t- Requirements\n\t- How to Use\n\t- Data Structure\n\n<!-- /MarkdownTOC -->\n\n\n---\n\n# Markup Description\n\n---\n\n## Voices\n\n**Voices** must be written in all caps, without any spaces before the name and followed by a colon.  \nExamples:\n\n- BASS 1:\n- VOICE 1:\n- LEAD:\n\nEvery connection described after a voice annotation will be assigned to that voice.\n\n---\n\n## Connections\n\nEvery connection (patch cable) must be annotated using the following format: **- Output Module (Output Label) >> Input Module (Input Label)**.  \nExamples:  \n\n```\n- Maths (Ch. 1 Unity) >> Polaris (CV 2)\n- Tides (Bi) >> Braids (Timbre)\n```\n\nWhile the >> indicator can be used to indicate a standard connection, it could (and should) also be replaced by more specific indicators according to the kind of signal being sent from the Output Module to the Input Module:  \n\n- \\>> for CV\n- -> for Audio\n- p> for Pitch (1v/oct or Hz/V)\n- g> for Gate\n- t> for Trigger\n- c> for Clock\n\nExamples:\n\n```\n- Metropolis (Pitch) p> Braids (1 V/Oct)\n- Pamela's Workout (1) c> Penta (Clk)\n- Braids (Out) -> Polaris (Input)\n```\n\n**Additional info:**\n\n- The manufacturer's name should only be included if the module's name is too generic (example: VB Modular ADSR).\n- Non-modular equipment (such as audio interfaces, recorders, and other synths) should be written in all caps: NAME OF GEAR (Input or Output).\n- While specific module names are preferable, they can also be replaced by more generic names such as VCA, ADSR, Oscillator, etc.\n\n**Extra arguments:**\n\nAdditional GraphViz arguments such as color, weight, and style can be appended to the connection line in between brackets and separated by commas.\n\nExample:\n```\n- Metropolis (Pitch) p> Braids (1 V/Oct) [color=red, weight=3]\n```\n\nSupported GraphViz arguments: color, weight, style, dir, and arrowtail.\n\n---\n\n## Parameters\n\nParameters can be annotated in 2 different ways: single line or multiline. Every parameter annotation must start with an asterisk character before the module name.\n\n**Single-line**  \n```\n* Function: Rise = 50% | Fall = 50% | Curve = 30%\n```\n\n**Multi-Line**\n```\n* Braids:  \n\t| Mode = CSAW  \n\t| Color = 50%  \n\t| Timbre = 50%  \n```\n\n**Additional info**\n\n- Parameter values can be written as knob / fader position (percentage), specific value followed by unit (5Hz, 10ms, etc.), or as a descriptive value (fast, slow, simple, complex, short, long).\n- Parameters are not assigned to any voice since the same module can be used in multiple voices.\n\n---\n\n## Comments:\n\nComments can be added to the patch by prepending two forward slashes (//).\n\nExample:\n\n```\n// This is a nice comment\n```\n\n---\n\n## Examples\n\n---\n\n### Example 1\n\n```\nVOICE 1:\n\t- Metropolis (Pitch) p> Braids (1v/oct) [weight=3]\n\t- Metropolis (Gate) g> Function (Trigger)\n\t- Braids (Out) -> Optomix (Ch1 Signal)\n\t- Function (+ Out) >> Optomix (Ch1 CV)\n\t- Function (- Out) >> Braids (Timbre CV)\n\t- Optomix (Out 1) -> AUDIO INTERFACE (input)\n\n\t* Metropolis:\n\t| BPM = 124\n\t| Swing = 0\n\t| Root = F\n\t| Scale = Minor\n\t| Mode = F. Forward\n\t| Stages = 16\n\n\t* Braids:\n\t| Mode = Fold\n\t| Timbre = 30%\n\t| Timbre CV = -20%\n\t| Color = 0%\n\n\t* Function: Rise = 50% | Fall = 50% | Curve = 30%\n\t* Optomix: Damp = 0% | Control = 100%\n```\n\n### Example 2\n\n```\nVOICE 1:\n\n\t- Metropolis (Pitch) p> Aether VCO (CV)\n\t- Metropolis (Gate) g> Maths (Ch 1 Trigger)\n\t- Metropolis (Gate) g> Maths (Ch 4 Trigger)\n\n\t* Aether VCO: LFO Freq = 5 | LFO PWM = 7\n\t- Aether VCO (Pulse) -> Mixer (Ch1)\n\t- Aether VCO (Sub 1) -> Tides (Clk)\n\t- Tides (Bi) -> Mixer (Ch2)\n\t- Aether VCO (Sub 2) -> Z3000 (HSync)\n\t- Z3000 (Saw) -> Mixer (Ch3)\n\n\t- MultiLFO (LFO 1) >> Tides (Smoothness)\n\t- MultiLFO (LFO 2 Triangle) >> Tides (Shape)\n\t- MultiLFO (LFO 3 Triangle) >> Z3000 (PWM)\n\t* MultiLFO:\n\t| LFO 1 Freq = 3.8\n\t| LFO 1 Shape = Sine\n\t| LFO 1 S&H = 0\n\t| LFO 2 Freq = 1\n\t| LFO 3 Freq = 1\n\t* Tides: PLL Mode = True | Freq = 60% | Smoothness = 70%\n\t* Z3000: Freq = 1pm\n\n\t- Maths (Ch 1) >> Multifilter (CV)\n\t- Maths (Ch 4) >> uVCA (Ch1 CV)\n\n\t- Mixer (Output) -> Multifilter (Input)\n\t- Multifilter (LPF) -> uVCA (Ch1 Input)\n\t- uVCA (Ch1 Output) -> AUDIO INTERFACE (In 3)\n\n```\n\n----\n\n# Parser\n\nThe PatchBook parser is a Python program that can read text files written in the PatchBook format and generate a JSON file.\n\n---\n## Requirements\n\n-  Python 3\n\n---\n## How to Use\n\nTo use the parser, download the python script, open the terminal and use the command:\n\n```\npython3 path/to/script/patchbook.py -file /path/to/textfile.txt\n```\n\nAfter loading the text file into the parser, you can use the following commands to process it:\n\n- **module**: Outputs a list of connections and parameters for a specific module.\n- **connections**: Prints a list of all connections organized by type (pitch, gate, clock, etc.).\n- **export**: Generates a JSON file based on the input text file.\n- **graph**: Generates a code that can be copied to pasted into the [Graphiz Online editor](https://dreampuf.github.io/GraphvizOnline/) to generate a signal flow chart for the patch (that can be downloaded as a SVG or PNG file). Non-programmers have the option to use the [Patchbook to GraphViz Online Converter](https://patchbook-converter.herokuapp.com) to create flowcharts without having to install Python and use the parser.\n\n![Example syncpll signal flow generated using GraphViz](/Images/graphviz-signal-flow.png?raw=true)\n\nAlternatively, any of the above commands may be invoked on the command line with a dash prefix, in which case the text file is parsed, the command is executed, and the program exits. This makes it possible to produce (for example) an SVG file in one step like so:\n\n```\npython3 path/to/script/patchbook.py -file /path/to/textfile.txt -graph | dot -Tsvg > /path/to/svgfile.svg\n```\n\nGraphs are constructed horizontally left-to-right by default, but you may use the **-dir** option to change the direction of the graph to vertical top-to-bottom like so:\n\n```\npython3 path/to/script/patchbook.py -file /path/to/textfile.txt -graph -dir DN\n```\n\n\n------\n## Data Structure\n\n![Patchbook Data Structure](/Images/datastructure.png?raw=true)\n\n----\n\nPatchbook was created by Ícaro Ferre / Spektro Audio.  \nTwitter: @icaroferre / @spektroaudio  \nhttp://spektroaudio.com/  \n"
  },
  {
    "path": "patchbook.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nPATCHBOOK MARKUP LANGUAGE & PARSER\nCREATED BY SPEKTRO AUDIO\nhttp://spektroaudio.com/\n\"\"\"\n\nimport sys\nimport re\nimport os\nimport argparse\nimport json\n\n# Parser INFO\nparserVersion = \"b3\"\n\n# Reset main dictionary\nmainDict = {\n    \"info\": {\"patchbook_version\": parserVersion},\n    \"modules\": {},\n    \"comments\": []\n}\n\n# Available connection types\nconnectionTypes = {\n    \"->\": \"audio\",\n    \">>\": \"cv\",\n    \"p>\": \"pitch\",\n    \"g>\": \"gate\",\n    \"t>\": \"trigger\",\n    \"c>\": \"clock\"\n}\n\n\n# Reset global variables\nlastModuleProcessed = \"\"\nlastVoiceProcessed = \"\"\n\n# Parse script arguments\nparser = argparse.ArgumentParser()\nparser.add_argument(\"-file\", type=str, default=\"\",\n                    help=\"Name of the text file that will be parsed (including extension)\")\nparser.add_argument(\"-debug\", type=int, default=0,\n                    help=\"Enable Debugging Mode\")\nparser.add_argument(\"-dir\", type=str, default=\"LR\",\n                    help=\"Graph direction: LR (left-to-right) or DN (top-to-bottom)\")\nparser.add_argument(\"-modules\", action=\"store_const\", const=\"modules\", dest=\"command\",\n                    help=\"Print all modules\")\nparser.add_argument(\"-print\", action=\"store_const\", const=\"print\", dest=\"command\",\n                    help=\"Print data structure\")\nparser.add_argument(\"-export\", action=\"store_const\", const=\"export\", dest=\"command\",\n                    help=\"Print JSON\")\nparser.add_argument(\"-connections\", action=\"store_const\", const=\"connections\", dest=\"command\",\n                    help=\"Print connections\")\nparser.add_argument(\"-graph\", action=\"store_const\", const=\"graph\", dest=\"command\",\n                    help=\"Print dot code for graph\")\nargs = parser.parse_args()\nfilename = args.file\ndebugMode = args.debug\ndirection = args.dir\nif args.command:\n    one_shot_command = args.command\n    quiet = True\nelse:\n    one_shot_command = None\n    quiet = False\n\nconnectionID = 0\n\n# Set up debugMode\nif args.debug == 1:\n    debugMode = True\nelse:\n    debugMode = False\n\n\ndef initial_print():\n    global quiet\n    if not quiet:\n        print()\n        print(\"██████████████████████████████\")\n        print(\"       PATCHBOOK PARSER       \")\n        print(\"   Created by Spektro Audio   \")\n        print(\"██████████████████████████████\")\n        print()\n        print(\"Version \" + parserVersion)\n        print()\n\n\ndef get_script_path():\n    # Get path to python script\n    return os.path.dirname(os.path.realpath(sys.argv[0]))\n\n\ndef getFilePath(filename):\n    try:\n        # Append script path to the filename\n        base_dir = get_script_path()\n        filepath = os.path.join(base_dir, filename)\n        if debugMode:\n            print(\"File path: \" + filepath)\n        return filepath\n    except IndexError:\n        pass\n\n\ndef parseFile(filename):\n    # This function reads the txt file and process each line.\n    global quiet\n    lines = []\n    try:\n        if not quiet: print(\"Loading file: \" + filename)\n        with open(filename, \"r\") as file:\n            for l in file:\n                lines.append(l)\n                regexLine(l)\n    except TypeError:\n        print(\"ERROR. Please add text file path after the script.\")\n    except FileNotFoundError:\n        print(\"ERROR. File not found.\")\n    if not quiet:\n        print(\"File successfully processed.\")\n        print()\n\n\ndef regexLine(line):\n    global lastModuleProcessed\n    global lastVoiceProcessed\n\n    if debugMode:\n        print()\n    if debugMode:\n        print(\"Processing: \" + line)\n\n    # CHECK FOR COMMENTS\n    if debugMode:\n        print(\"Checking input for comments...\")\n    re_filter = re.compile(r\"^\\/\\/\\s?(.+)$\")  # Regex for \"// Comments\"\n    re_results = re_filter.search(line.strip())\n    try:\n        comment = re_results.group().replace(\"//\", \"\").strip()\n        if debugMode:\n            print(\"New comment found: \" + comment)\n            addComment(comment)\n        return\n    except AttributeError:\n        pass\n\n    # CHECK FOR VOICES\n    if debugMode:\n        print(\"Cheking input for voices...\")\n    re_filter = re.compile(r\"^(.+)\\:$\")  # Regex for \"VOICE 1:\"\n    re_results = re_filter.search(line)\n    try:\n        # For some reason the Regex filter was still detecting parameter declarations as voices,\n        # so I'm also running the results through an if statement.\n        results = re_results.group().replace(\":\", \"\")\n        if \"*\" not in results and \"-\" not in results and \"|\" not in results:\n            if debugMode:\n                print(\"New voice found: \" + results.upper())\n            lastVoiceProcessed = results.upper()\n            return\n    except AttributeError:\n        pass\n\n    # CHECK FOR CONNECTIONS\n    if debugMode:\n        print(\"Cheking input for connections...\")\n    re_filter = re.compile(\n        r\"\\-\\s(.+)[(](.+)[)]\\s(\\>\\>|\\-\\>|[a-z]\\>)\\s(.+)[(](.+)[)]\\s(\\[.+\\])?$\")\n    re_results = re_filter.search(line)\n    try:\n        results = re_results.groups()\n        voice = lastVoiceProcessed\n        if len(results) == 6:\n            if debugMode:\n                print(\"New connection found, parsing info...\")\n            # args = parseArguments(results[5])\n            # results = results[:5]\n            addConnection(results, voice)\n            return\n    except AttributeError:\n        pass\n\n    # CHECK PARAMETERS\n    if debugMode:\n        print(\"Checking for parameters...\")\n    # If single-line parameter declaration:\n    re_filter = re.compile(r\"^\\*\\s(.+)\\:\\s?(.+)?$\")\n    re_results = re_filter.search(line.strip())\n    try:\n        # Get module name\n        results = re_results.groups()\n        module = results[0].strip().lower()\n        if debugMode:\n            print(\"New module found: \" + module)\n        if results[1] != None:\n            # If parameters are also declared\n            parameters = results[1].split(\" | \")\n            for p in parameters:\n                p = p.split(\" = \")\n                addParameter(module, p[0].strip().lower(), p[1].strip())\n            return\n        elif results[1] == None:\n            if debugMode:\n                print(\"No parameters found. Storing module as global variable...\")\n            lastModuleProcessed = module\n            return\n    except AttributeError:\n        pass\n\n    # If multi-line parameter declaration:\n    if \"|\" in line and \"=\" in line and \"*\" not in line:\n        module = lastModuleProcessed.lower()\n        if debugMode:\n            print(\"Using global variable: \" + module)\n        parameter = line.split(\" = \")[0].replace(\"|\", \"\").strip().lower()\n        value = line.split(\" = \")[1].strip()\n        addParameter(module, parameter, value)\n        return\n\n\ndef parseArguments(args):\n    # This method takes an arguments string like \"[color = blue]\" and converts it to a dictionary\n    args_string = args.replace(\"[\", \"\").replace(\"]\", \"\")\n    args_array = args_string.split(\",\")\n    args_dict = {}\n\n    if debugMode:\n        print(\"Parsing arguments: \" + args)\n\n    for item in args_array:\n        item = item.split(\"=\")\n        name = item[0].strip()\n        value = item[1].strip()\n        args_dict[name] = value\n        if debugMode:\n            print(name + \" = \" + value)\n\n    if debugMode:\n        print(\"All arguments processes.\")\n\n    return args_dict\n\n\ndef addConnection(list, voice=\"none\"):\n    global mainDict\n    global connectionTypes\n    global connectionID\n\n    connectionID += 1\n\n    if debugMode:\n        print(\"Adding new connection...\")\n        print(\"-----\")\n\n    output_module = list[0].lower().strip()\n    output_port = list[1].lower().strip()\n\n    if debugMode:\n        print(\"Output module: \" + output_module)\n        print(\"Output port: \" + output_port)\n\n    try:\n        connection_type = connectionTypes[list[2].lower()]\n        if debugMode:\n            print(\"Matched connection type: \" + connection_type)\n    except KeyError:\n        print(\"Invalid connection: \" + list[2])\n        connection_type = \"cv\"\n\n    input_module = list[3].lower().strip()\n    input_port = list[4].lower().strip()\n\n    if list[5] is not None:\n        arguments = parseArguments(list[5])\n    else:\n        arguments = {}\n\n    if debugMode:\n        print(\"Input module: \" + input_module)\n        print(\"Input port: \" + output_port)\n\n    checkModuleExistance(output_module, output_port, \"out\")\n    checkModuleExistance(input_module, input_port, \"in\")\n\n    if debugMode:\n        print(\"Appending output and input connections to mainDict...\")\n\n    output_dict = {\n        \"input_module\": input_module,\n        \"input_port\": input_port,\n        \"connection_type\": connection_type,\n        \"voice\": voice,\n        \"id\": connectionID}\n\n    input_dict = {\n        \"output_module\": output_module,\n        \"output_port\": output_port,\n        \"connection_type\": connection_type,\n        \"voice\": voice,\n        \"id\": connectionID}\n\n    for key in arguments:\n        output_dict[key] = arguments[key]\n        input_dict[key] = arguments[key]\n\n    mainDict[\"modules\"][output_module][\"connections\"][\"out\"][output_port].append(\n        output_dict)\n    mainDict[\"modules\"][input_module][\"connections\"][\"in\"][input_port] = input_dict\n    if debugMode:\n        print(\"-----\")\n\n\ndef checkModuleExistance(module, port=\"port\", direction=\"\"):\n    global mainDict\n\n    if debugMode:\n        print(\"Checking if module already existing in main dictionary: \" + module)\n\n    # Check if module exists in main dictionary\n    if module not in mainDict[\"modules\"]:\n        mainDict[\"modules\"][module] = {\n            \"parameters\": {},\n            \"connections\": {\"out\": {}, \"in\": {}}\n        }\n\n    # If it exists, check if the port exists\n    if direction == \"in\":\n        if port not in mainDict[\"modules\"][module][\"connections\"][\"in\"]:\n            mainDict[\"modules\"][module][\"connections\"][\"in\"][port] = []\n\n    if direction == \"out\":\n        if port not in mainDict[\"modules\"][module][\"connections\"][\"out\"]:\n            mainDict[\"modules\"][module][\"connections\"][\"out\"][port] = []\n\n\ndef addParameter(module, name, value):\n    checkModuleExistance(module)\n    # Add parameter to mainDict\n    if debugMode:\n        print(\"Adding parameter: \" + module + \" - \" + name + \" - \" + value)\n    mainDict[\"modules\"][module][\"parameters\"][name] = value\n\n\ndef addComment(value):\n    mainDict[\"comments\"].append(value)\n\n\ndef askCommand(command=None):\n    global one_shot_command\n    if one_shot_command:\n        command = one_shot_command\n    if not command:\n        command = input(\"> \").lower().strip()\n\n    if command == \"module\":\n        detailModule()\n    elif command == \"modules\":\n        detailModule(all=True)\n    elif command == \"print\":\n        printDict()\n    elif command == \"export\":\n        exportJSON()\n    elif command == \"connections\":\n        printConnections()\n    elif command == \"graph\":\n        graphviz()\n    else:\n        print(\"Invalid command, please try again.\")\n\n    if one_shot_command:\n        return\n    askCommand()\n\ndef _print_module(module):\n    global mainDict, quiet\n    print(\"-------\")\n    print(\"Showing information for module: \" + module.upper())\n    print()\n    print(\"Inputs:\")\n    for c in mainDict[\"modules\"][module][\"connections\"][\"in\"]:\n        keyvalue = mainDict[\"modules\"][module][\"connections\"][\"in\"][c]\n        print(keyvalue[\"output_module\"].title() + \" (\" + keyvalue[\"output_port\"].title(\n        ) + \") > \" + c.title() + \" - \" + keyvalue[\"connection_type\"].title())\n    print()\n\n    print(\"Outputs:\")\n    for x in mainDict[\"modules\"][module][\"connections\"][\"out\"]:\n        port = mainDict[\"modules\"][module][\"connections\"][\"out\"][x]\n        for c in port:\n            keyvalue = c\n            print(x.title() + \" > \" + keyvalue[\"input_module\"].title() + \" (\" + keyvalue[\"input_port\"].title(\n            ) + \") \" + \" - \" + keyvalue[\"connection_type\"].title() + \" - \" + keyvalue[\"voice\"])\n    print()\n\n    print(\"Parameters:\")\n    for p in mainDict[\"modules\"][module][\"parameters\"]:\n        value = mainDict[\"modules\"][module][\"parameters\"][p]\n        print(p.title() + \" = \" + value)\n    print()\n\n    if not quiet: print(\"-------\")\n\ndef detailModule(all=False):\n    global mainDict\n    if not all:\n        module = input(\"Enter module name: \").lower()\n        if module in mainDict[\"modules\"]:\n            _print_module(module)\n    else:\n        for module in mainDict[\"modules\"]:\n            _print_module(module)\n\n\ndef printConnections():\n    print()\n    print(\"Printing all connections by type...\")\n    print()\n\n    for ctype in connectionTypes:\n        ctype_name = connectionTypes[ctype]\n        print(\"Connection type: \" + ctype_name)\n        # For each module\n        for module in mainDict[\"modules\"]:\n            # Get all outgoing connections:\n            connections = mainDict[\"modules\"][module][\"connections\"][\"out\"]\n            for c in connections:\n                connection = connections[c]\n                for subc in connection:\n                    # print(connection)\n                    if subc[\"connection_type\"] == ctype_name:\n                        print(module.title(\n                        ) + \" > \" + subc[\"input_module\"].title() + \" (\" + subc[\"input_port\"].title() + \") \")\n        print()\n\n\ndef exportJSON():\n    # Exports mainDict as json file\n    # name = filename.split(\".\")[0]\n    # filepath = getFilePath(name + '.json')\n    # print(\"Exporting dictionary as file: \" + filepath)\n    # with open(filepath, 'w') as fp:\n    #     json.dump(mainDict, fp)\n    print(json.dumps(mainDict))\n\n\ndef graphviz():\n    global quiet, direction\n    linetypes = {\n        \"audio\": {\"style\": \"bold\"},\n        \"cv\": {\"color\": \"gray\"},\n        \"gate\": {\"color\": \"red\", \"style\": \"dashed\"},\n        \"trigger\": {\"color\": \"orange\", \"style\": \"dashed\"},\n        \"pitch\": {\"color\": \"blue\"},\n        \"clock\": {\"color\": \"purple\", \"style\": \"dashed\"}\n    }\n    if direction == \"DN\":\n        rank_dir_token = \"rankdir = BT;\\n\"\n        from_token = \":s  -> \"\n        to_token = \":n  \"\n    else:\n        rank_dir_token = \"rankdir = LR;\\n\"\n        from_token = \":e  -> \"\n        to_token = \":w  \"\n    if not quiet:\n        print(\"Generating signal flow code for GraphViz.\")\n        print(\"Copy the code between the line break and paste it into https://dreampuf.github.io/GraphvizOnline/ to download a SVG / PNG chart.\")\n    conn = []\n    total_string = \"\"\n    if not quiet: print(\"-------------------------\")\n    print(\"digraph G{\\n\" + rank_dir_token + \"splines = polyline;\\nordering=out;\")\n    total_string += \"digraph G{\\n\" + rank_dir_token + \"splines = polyline;\\nordering=out;\\n\"\n    for module in sorted(mainDict[\"modules\"]):\n        # Get all outgoing connections:\n        outputs = mainDict[\"modules\"][module][\"connections\"][\"out\"]\n        module_outputs = \"\"\n        out_count = 0\n        for out in sorted(outputs):\n            out_count += 1\n            out_formatted = \"_\" + re.sub('[^A-Za-z0-9]+', '', out)\n            module_outputs += \"<\" + out_formatted + \"> \" + out.upper()\n            if out_count < len(outputs.keys()):\n                module_outputs += \" | \"\n            connections = outputs[out]\n            for c in connections:\n                line_style_array = []\n                graphviz_parameters = [\n                    \"color\", \"weight\", \"style\", \"arrowtail\", \"dir\"]\n                for param in graphviz_parameters:\n                    if param in c:\n                        line_style_array.append(param + \"=\" + c[param])\n                    elif param in linetypes[c[\"connection_type\"]]:\n                        line_style_array.append(\n                            param + \"=\" + linetypes[c[\"connection_type\"]][param])\n                if len(line_style_array) > 0:\n                    line_style = \"[\" + ', '.join(line_style_array) + \"]\"\n                else:\n                    line_style = \"\"\n                in_formatted = \"_\" + \\\n                    re.sub('[^A-Za-z0-9]+', '', c[\"input_port\"])\n                connection_line = module.replace(\" \", \"\") + \":\" + out_formatted + from_token + \\\n                    c[\"input_module\"].replace(\n                        \" \", \"\") + \":\" + in_formatted + to_token + line_style\n                conn.append([c[\"input_port\"], connection_line])\n\n        # Get all incoming connections:\n        inputs = mainDict[\"modules\"][module][\"connections\"][\"in\"]\n        module_inputs = \"\"\n        in_count = 0\n        for inp in sorted(inputs):\n            inp_formatted = \"_\" + re.sub('[^A-Za-z0-9]+', '', inp)\n            in_count += 1\n            module_inputs += \"<\" + inp_formatted + \"> \" + inp.upper()\n            if in_count < len(inputs.keys()):\n                module_inputs += \" | \"\n\n        # Get all parameters:\n        params = mainDict[\"modules\"][module][\"parameters\"]\n        module_params = \"\"\n        param_count = 0\n        for par in sorted(params):\n            param_count += 1\n            module_params += par.title() + \" = \" + params[par]\n            if param_count < len(params.keys()):\n                module_params += r'\\n'\n\n        # If module contains parameters\n        if module_params != \"\":\n            # Add them below module name\n            middle = \"{{\" + module.upper() + \"}|{\" + module_params + \"}}\"\n        else:\n            # Otherwise just display module name\n            middle = module.upper()\n\n        final_box = module.replace(\n            \" \", \"\") + \"[label=\\\"{ {\" + module_inputs + \"}|\" + middle + \"| {\" + module_outputs + \"}}\\\"  shape=Mrecord]\"\n        print(final_box)\n        total_string += final_box + \"; \"\n\n    # Print Connections\n    for c in sorted(conn):\n        print(c[1])\n        total_string += c[1] + \"; \"\n\n    if len(mainDict[\"comments\"]) != 0:\n        format_comments = \"\"\n        comments_count = 0\n        for comment in mainDict[\"comments\"]:\n            comments_count += 1\n            format_comments += \"{\" + comment + \"}\"\n            if comments_count < len(mainDict[\"comments\"]):\n                format_comments += \"|\"\n        format_comments = \"comments[label=<{{{<b>PATCH COMMENTS</b>}|\" + format_comments + \"}}>  shape=Mrecord]\"\n        print(format_comments)\n\n    print(\"}\")\n    total_string += \"}\"\n\n    if not quiet:\n        print(\"-------------------------\")\n        print()\n    return total_string\n\n\ndef printDict():\n    global mainDict\n    for key in mainDict[\"modules\"]:\n        print(key.title() + \": \" + str(mainDict[\"modules\"][key]))\n\n\nif __name__ == \"__main__\":\n    initial_print()\n    parseFile(filename)\n    askCommand()\n"
  }
]