summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRicardo Wurmus <rekado@elephly.net>2024-04-30 06:40:17 +0200
committerRicardo Wurmus <rekado@elephly.net>2024-04-30 06:46:47 +0200
commit491ae9ba960e65325f3676bff6dec0c3bc74170b (patch)
treecfb915ec7aa022ee4b9e896572b0d0397f220078
Initial commit.
-rw-r--r--README.org31
-rw-r--r--faust-ksoloti-object.c65
-rwxr-xr-xfaust2axo.py128
-rw-r--r--manifest.scm1
4 files changed, 225 insertions, 0 deletions
diff --git a/README.org b/README.org
new file mode 100644
index 0000000..2c93887
--- /dev/null
+++ b/README.org
@@ -0,0 +1,31 @@
+#+TITLE: Convert Faust DSP to Axoloti objects
+
+Enter a development environment:
+
+#+begin_example sh
+guix shell -m manifest.scm
+#+end_example
+
+Take a Faust DSP program:
+
+#+begin_src faust
+import("stdfaust.lib");
+va = library("vaeffects.lib");
+
+freq = hslider("cutoff [unit:Hz]", 10000, 0, 10000, 0.1) : si.smoo;
+res = hslider("resonance", 0, 0, 1, 0.01) : si.smoo;
+
+process = va.moog_vcf_2b(res, freq);
+#+end_src
+
+And convert it to a crude Axoloti object:
+
+#+begin_src sh
+./faust2axo.py -i moog.dsp -o test.axo
+#+end_src
+
+
+
+* License
+
+GPLv3+
diff --git a/faust-ksoloti-object.c b/faust-ksoloti-object.c
new file mode 100644
index 0000000..82e575d
--- /dev/null
+++ b/faust-ksoloti-object.c
@@ -0,0 +1,65 @@
+<objdefs>
+ <obj.normal id="moog_vcf_2b" uuid="0009f089dced01ba64f7e22ac77fd78fbec47db9" sha="000d84e43d5934e94f32e1d50318f8feede69769">
+ <sDescription>Moog VCF (moog_vcf_2b)</sDescription>
+ <author>Ricardo Wurmus (Ksoloti), Julius O. Smith III (Faust)</author>
+ <license>STK-4.3</license>
+ <helpPatch/>
+ <includes>
+ <include>math.h</include>
+ <include>axoloti.h</include>
+ </includes>
+ <inlets>
+ <frac32buffer name="in" description="filter input"/>
+ <<INLETS>>
+ </inlets>
+ <outlets>
+ <frac32buffer name="out" description="filter output"/>
+ </outlets>
+ <displays/>
+ <params>
+ <<PARAMS>>
+ </params>
+ <attribs/>
+ <code.declaration><![CDATA[
+float fmaxf (float a, float b) {
+ return a > b ? a : b;
+}
+float fminf (float a, float b) {
+ return a < b ? a : b;
+}
+float tanf(float x) {
+ return sinf(x) / cosf(x);
+}
+
+//----------------------------------------------------------------------------
+// FAUST generated signal processor
+//----------------------------------------------------------------------------
+
+<<includeIntrinsic>>
+
+<<includeclass>>
+
+mydsp mdsp;
+]]></code.declaration>
+ <code.init><![CDATA[initmydsp(&mdsp, 48000.0);]]></code.init>
+ <code.dispose><![CDATA[//deletemydsp(mdsp);]]></code.dispose>
+ <code.krate><![CDATA[// Calculate audio
+<<SLIDERS>>
+
+float inf[BUFSIZE] = {};
+float outf[BUFSIZE] = {};
+
+for (uint8_t i = 0; i < BUFSIZE; i++) {
+ inf[i] = q27_to_float(inlet_in[i]);
+}
+
+float* inp = (float*) &inf;
+float* outp = (float*) &outf;
+
+computemydsp(&mdsp, BUFSIZE, *inp, *outp);
+
+for (uint8_t i = 0; i < BUFSIZE; i++) {
+ outlet_out[i] = float_to_q27(outf[i]);
+}]]></code.krate>
+ </obj.normal>
+</objdefs>
diff --git a/faust2axo.py b/faust2axo.py
new file mode 100755
index 0000000..2157221
--- /dev/null
+++ b/faust2axo.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+
+import sys
+import argparse
+import subprocess
+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 = ""
+ sliders = xml.findall(".//ui/activewidgets/widget[@type='hslider']")
+ for slider in sliders:
+ attributes = slider.attrib
+ varname = slider.find("varname").text
+ label = slider.find("label").text
+ max = slider.find("max").text
+
+ inlets += f"<frac32 name=\"{label}\" />\n"
+ params += f"<frac32.u.map name=\"{label}\" />\n"
+
+ # TODO: summation and conversion based on type
+ output += f"int32_t {label} = mtof48k_ext_q31(param_{label} + inlet_{label});\n"
+ output += f"mdsp.{varname} = fminf(1.0f, q27_to_float({label})) * {max};\n"
+ return (inlets, params, output)
+
+
+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")
+ # - replace scaling factors for sliders
+ # - replace metadata
+
+ inlets, params, conversions = process_sliders(xml)
+
+ seen_start = False
+ seen_end = False
+ skip_ifdef = False
+
+ 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 "<<INLETS>>" in line:
+ outfile.write(line.replace("<<INLETS>>", inlets))
+ continue
+
+ if "<<PARAMS>>" in line:
+ outfile.write(line.replace("<<PARAMS>>", params))
+ continue
+
+ if "<<SLIDERS>>" in line:
+ outfile.write(line.replace("<<SLIDERS>>", conversions))
+ continue
+
+ if line.startswith("#ifdef __cplusplus"):
+ skip_ifdef = True
+
+ if skip_ifdef:
+ if line.startswith("#endif"):
+ skip_ifdef = False
+ else:
+ continue
+
+ 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)
diff --git a/manifest.scm b/manifest.scm
new file mode 100644
index 0000000..9796782
--- /dev/null
+++ b/manifest.scm
@@ -0,0 +1 @@
+(specifications->manifest (list "python" "python-black" "faust"))