summaryrefslogtreecommitdiff
path: root/faust2axo.py
blob: 5bf09a1fb8972bf3fa98eb30648a71522009ce51 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3

import sys
import argparse
import subprocess
import tempfile
import uuid
import xml.etree.ElementTree as ET


def run_faust(dsp_file, output_filename):
    """Run Faust on the DSP_FILE, write the output to OUTPUT_FILENAME, and return the parsed XML tree of the description file."""
    with open(output_filename, "w") as outfile:
        result = subprocess.run(
            [
                "faust",
                "-xml",  # generate description
                "-lang",
                "c",  # C code, please
                "-scal",  # non-vectorized code
                "-light",  # keep it light
                "-a",
                "faust-ksoloti-object.c",  # architecture file
                dsp_file,
            ],
            stdout=outfile,
            check=True,
        )
    return result


def process_sliders(xml):
    """Process sliders and return inlets, params, and conversions."""
    inlets = ""
    params = ""
    output = ""
    widgets = xml.findall(".//ui/activewidgets/widget[@type='hslider']")
    for widget in widgets:
        varname = widget.find("varname").text
        label = widget.find("label").text
        max = widget.find("max").text
        maybe_unit = widget.find("meta[@key='unit']")
        pitch = isinstance(maybe_unit, ET.Element) and maybe_unit.text == "Hz"

        if pitch:
            param_type = "frac32.s.map.pitch"
        else:
            param_type = "frac32.u.map"

        inlets += f'<frac32 name="{label}" />\n'
        params += f'<{param_type} name="{label}" />\n'

        # TODO: summation and conversion based on type
        if pitch:
            output += (
                f"uint32_t {label} = mtof48k_ext_q31(param_{label} + inlet_{label});\n"
            )
        else:
            output += f"uint32_t {label} = param_{label} + inlet_{label};\n"
        output += f"mdsp.{varname} = fminf(1.0f, q27_to_float({label})) * {max};\n"
    return (inlets, params, output)


def safe_extract_text(xml, xpath, alternative):
    result = xml.find(xpath)
    if isinstance(result, ET.Element):
        return result.text
    else:
        return alternative


def massage_output(oldfile, newfile, xml):
    """
    Process the Faust output.
    """

    # - throw everything before <objdefs> away
    # - throw everything after </objdefs> away
    # - remove extern "C" chunk (wrapped in "#ifdef __cplusplus")
    # - inject params and inlets
    # - replace scaling factors for sliders
    # - replace metadata

    id = uuid.uuid4()
    author = safe_extract_text(xml, "./author", "unknown")
    name = safe_extract_text(xml, "./name", "unnamed")
    license = safe_extract_text(xml, "./license", "CC-SA 4.0")
    description = safe_extract_text(
        xml, "./description", "Generated from Faust DSP code."
    )

    inlets, params, conversions = process_sliders(xml)

    seen_start = False
    seen_end = False
    skip_ifdef = False

    replacements = {
        "UUID": str(id),
        "AUTHOR": author,
        "NAME": name,
        "LICENSE": license,
        "DESCRIPTION": description,
        "INLETS": inlets,
        "PARAMS": params,
        "SLIDERS": conversions,
    }

    with open(oldfile, "r", encoding="utf-8") as infile, open(
        newfile, "w", encoding="utf-8"
    ) as outfile:
        for line in infile:
            if not seen_start and line.startswith("<objdefs>"):
                seen_start = True
            if not seen_end and line.startswith("</objdefs>"):
                seen_end = True
            if seen_end:
                outfile.write(line)
                return

            if seen_start:
                if line.startswith("#ifdef __cplusplus"):
                    skip_ifdef = True

                if skip_ifdef:
                    if line.startswith("#endif"):
                        skip_ifdef = False
                    continue

                for key, value in replacements.items():
                    if "<<" + key + ">>" in line:
                        line = line.replace("<<" + key + ">>", value)

                outfile.write(line)
            else:
                continue


def main(faust_input_file, axo_output_file):
    with tempfile.NamedTemporaryFile() as temp:
        run_faust(faust_input_file, temp.name)

        # Parse generated XML description
        dsp_xml = faust_input_file + ".xml"
        xml = ET.parse(dsp_xml).getroot()

        # Modify Axoloti object file
        massage_output(temp.name, axo_output_file, xml)


def arguments():
    parser = argparse.ArgumentParser()
    parser.add_argument("-i", "--input", type=str, required=True)
    parser.add_argument("-o", "--output", type=str, required=True)
    return parser.parse_args()


if __name__ == "__main__":
    args = arguments()
    main(args.input, args.output)