#!/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'\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 away # - throw everything after 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(""): seen_start = True if not seen_end and line.startswith(""): 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)