#!/usr/bin/env python3
import sys
import argparse
import subprocess
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):
axo_temp_output_file = axo_output_file + ".temp"
run_faust(faust_input_file, axo_temp_output_file)
# Parse generated XML description
dsp_xml = faust_input_file + ".xml"
xml = ET.parse(dsp_xml).getroot()
# Modify Axoloti object file
massage_output(axo_temp_output_file, 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)