diff --git a/CMakeLists.txt b/CMakeLists.txt index dce2d0da..c4b982fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ option_ex (SFIZZ_RELEASE_ASSERTS "Forced assertions in release builds" OFF) include (SfizzConfig) include (SfizzDeps) +include (SfizzFaust) # Don't use IPO in non Release builds include (CheckIPO) diff --git a/cmake/SfizzFaust.cmake b/cmake/SfizzFaust.cmake new file mode 100644 index 00000000..a69ee115 --- /dev/null +++ b/cmake/SfizzFaust.cmake @@ -0,0 +1,72 @@ +include(CMakeParseArguments) + +option(SFIZZ_RECOMPILE_FAUST "Recompile faust sources" OFF) + +if(SFIZZ_RECOMPILE_FAUST) + find_program(RDMD "rdmd") + if(NOT RDMD) + message(FATAL_ERROR "rdmd is missing, it is required for regenerating faust sources.") + endif() +endif() + +function(add_faust_command INPUT OUTPUT) + set(_options ONE_SAMPLE DOUBLE IN_PLACE VECTORIZE MATH_APPROXIMATION) + set(_one_args PROCESS_NAME CLASS_NAME SUPERCLASS_NAME) + set(_multi_args IMPORT_DIRS) + cmake_parse_arguments(_FAUST "${_options}" "${_one_args}" "${_multi_args}" ${ARGN}) + if(NOT SFIZZ_RECOMPILE_FAUST) + return() + endif() + if(NOT RDMD) + return() + endif() + if(NOT INPUT) + message(FATAL_ERROR "No input file given.") + endif() + if(NOT OUTPUT) + message(FATAL_ERROR "No output file given.") + endif() + set(_cmd "${RDMD}" "${PROJECT_SOURCE_DIR}/scripts/faustwrap.d") + if(NOT IS_ABSOLUTE "${INPUT}") + set(INPUT "${CMAKE_CURRENT_SOURCE_DIR}/${INPUT}") + endif() + if(NOT IS_ABSOLUTE "${OUTPUT}") + set(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/${OUTPUT}") + endif() + get_filename_component(_output_dir "${OUTPUT}" DIRECTORY) + file(MAKE_DIRECTORY "${_output_dir}") + list(APPEND _cmd "-o" "${OUTPUT}" "${INPUT}") + if(_FAUST_ONE_SAMPLE) + list(APPEND _cmd "--os") + endif() + if(_FAUST_DOUBLE) + list(APPEND _cmd "--double") + endif() + if(_FAUST_IN_PLACE) + list(APPEND _cmd "--inpl") + endif() + if(_FAUST_VECTORIZE) + list(APPEND _cmd "--vec") + endif() + if(_FAUST_MATH_APPROXIMATION) + list(APPEND _cmd "--mapp") + endif() + if(_FAUST_PROCESS_NAME) + list(APPEND _cmd "--pn" "${_FAUST_PROCESS_NAME}") + endif() + if(_FAUST_CLASS_NAME) + list(APPEND _cmd "--cn" "${_FAUST_CLASS_NAME}") + endif() + if(_FAUST_SUPERCLASS_NAME) + list(APPEND _cmd "--scn" "${_FAUST_SUPERCLASS_NAME}") + endif() + if (_FAUST_IMPORT_DIRS) + foreach(_dir IN LISTS _FAUST_IMPORT_DIRS) + if(NOT IS_ABSOLUTE "${_dir}") + set(_dir "${CMAKE_CURRENT_SOURCE_DIR}/${_dir}") + endif() + list(APPEND _cmd "--import-dir" "${_dir}") + endforeach() + endif() + add_custom_command(OUTPUT "${OUTPUT}" COMMAND ${_cmd} DEPENDS "${INPUT}") +endfunction() diff --git a/scripts/faustwrap.d b/scripts/faustwrap.d new file mode 100755 index 00000000..aad1e47d --- /dev/null +++ b/scripts/faustwrap.d @@ -0,0 +1,393 @@ +#!/usr/bin/env rdmd + +import std.conv; +import std.algorithm; +import std.getopt; +import std.process; +import std.regex; +import std.uni; +import std.ascii; +import std.string; +import std.array; +import std.stdio; +import core.stdc.stdlib; + +enum string prologue = `#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#endif +`; + +enum string epilogue = ` +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif +#undef FAUSTFLOAT +#undef FAUSTCLASS +`; + +void main(string[] args) +{ + struct Opts { + string outputPath; + string className = "mydsp"; + string superclassName = null; + bool oneSample = false; + bool doublePrecision = false; + bool inPlace = false; + bool vectorize = false; + bool mathApproximation = false; + string processName = null; + string[] importDirs; + } + + Opts opts; + + auto optInfo = getopt(args, + "out|o", "Output path", &opts.outputPath, + "cn", "Class name", &opts.className, + "scn", "Superclass name", &opts.superclassName, + "os", "One sample", &opts.oneSample, + "double", "Double precision", &opts.doublePrecision, + "inpl", "In-place", &opts.inPlace, + "vec", "Vectorization", &opts.vectorize, + "mapp", "Math approximation", &opts.mathApproximation, + "pn", "Process name", &opts.processName, + "import-dir|I", "Import directory", &opts.importDirs); + + if (optInfo.helpWanted) + { + defaultGetoptPrinter("MyFaust", optInfo.options); + return; + } + + if (args.length != 2) + { + writeln("You must indicate exactly 1 input file."); + exit(1); + } + + string[] cmd = [getFaust()]; + cmd ~= "-cn"; + cmd ~= opts.className; + if (opts.superclassName) + { + cmd ~= "-scn"; + cmd ~= opts.superclassName; + } + if (opts.oneSample) + cmd ~= "-os"; + if (opts.doublePrecision) + cmd ~= "-double"; + if (opts.inPlace) + cmd ~= "-inpl"; + if (opts.vectorize) + cmd ~= "-vec"; + if (opts.mathApproximation) + cmd ~= "-mapp"; + if (opts.processName) + { + cmd ~= "-pn"; + cmd ~= opts.processName; + } + foreach (string dir; opts.importDirs) + { + cmd ~= "-I"; + cmd ~= dir; + } + cmd ~= args[1]; + + auto result = execute(cmd, null, Config.stderrPassThrough|Config.suppressConsole); + if (result.status != 0) + exit(1); + + string code = result.output; + Parameter[] params = findParameters(code); + + code = removeVirtualKeyword(code); + foreach (string method; ["metadata", "getInputRate", "getOutputRate", "clone", "buildUserInterface"]) + code = removeMethod(code, method); + foreach (string method; ["getNumInputs", "getNumOutputs"]) + code = makeMethodStaticConstexpr(code, method); + if (!opts.superclassName) + code = removeSuperclass(code); + code = makePointerArgsConst(code, opts.oneSample); + code = addParameters(code, params); + + foreach (string method; ["compute", "classInit", "instanceConstants", "instanceResetUserInterface", "instanceClear", "init", "instanceInit"]) + code = addMethodStartEnd(code, method); + code = addClassStartEnd(code); + + File outFile = stdout; + if (opts.outputPath) + outFile = File(opts.outputPath, "w"); + outFile.writef("%s%s%s", prologue, code, epilogue); + outFile.flush(); +} + +final class Parameter +{ + string name; + string var; + bool readonly; +}; + +Parameter[] findParameters(string code) +{ + Parameter[] params; + + auto expr = regex(`->add(Button|CheckButton|VerticalSlider|HorizontalSlider|NumEntry|HorizontalBargraph|VerticalBargraph)\("([^"]*)", &([a-zA-Z0-9_]+)`); + + foreach (string line; code.lineSplitter) + { + auto match = line.matchFirst(expr); + if (match) + { + Parameter param = new Parameter; + param.name = match[2]; + param.var = match[3]; + param.readonly = ["HorizontalBargraph", "VerticalBargraph"].canFind(match[1]); + params ~= param; + } + } + + return params; +} + +string removeVirtualKeyword(string code) +{ + auto expr = regex(`\bvirtual\s*\b`); + return code.replaceAll(expr, ""); +} + +string removeMethod(string code, string method) +{ + string[] newLines; + newLines.reserve(1024); + + auto expr = regex(`^(\s*).*\b` ~ method.escaper.to!string ~ `\s*\(.*\{\s*$`); + + bool inMethod = false; + string eom = null; + + foreach (string line; code.lineSplitter) + { + if (inMethod) + { + if (line.stripRight == eom) + inMethod = false; + } + else + { + auto match = line.matchFirst(expr); + if (match) + { + inMethod = true; + eom = match[1] ~ '}'; + } + else + newLines ~= line; + } + } + + return newLines.join('\n'); +} + +string makeMethodStaticConstexpr(string code, string method) +{ + string[] newLines; + newLines.reserve(1024); + + auto expr = regex(`^(\s*)(.*\b` ~ method.escaper.to!string ~ `\s*\(.*\{\s*)$`); + + foreach (string line; code.lineSplitter) + { + auto match = line.matchFirst(expr); + if (match) + newLines ~= match[1] ~ "static constexpr " ~ match[2]; + else + newLines ~= line; + } + + return newLines.join('\n'); +} + +string removeSuperclass(string code) +{ + auto expr = regex(`\s*:\s*public\s+dsp\s*`); + return code.replaceFirst(expr, " "); +} + +string makePointerArgsConst(string code, bool oneSample) +{ + if (!oneSample) + { + auto expr = regex(`\bvoid\s+compute\s*\(\s*([^,)]+)\s*,\s*([^,)]+)\s*,\s*([^,)]+)\s*\)`); + auto match = code.matchFirst(expr); + + string arg1 = match[1]; + string arg2 = match[2]; + string arg3 = match[3]; + + auto exprArg = regex(`^FAUSTFLOAT\s*\*\s*\*`); + arg2 = arg2.replaceFirst(exprArg, "FAUSTFLOAT const* const*"); + arg3 = arg3.replaceFirst(exprArg, "FAUSTFLOAT* const*"); + + ulong start = match[0].ptr - code.ptr; + ulong end = start + match[0].length; + code = format("%svoid compute(%s, %s, %s)%s", code[0..start], arg1, arg2, arg3, code[end..$]); + + // + auto exprStmt = regex(`FAUSTFLOAT\s*\*\s*(input\d+)`); + code = code.replaceAll(exprStmt, "FAUSTFLOAT const* $1"); + } + else + { + auto expr = regex(`\bvoid\s+compute\s*\(\s*([^,)]+)\s*,\s*([^,)]+)\s*,\s*([^,)]+)\s*,\s*([^,)]+)\s*\)`); + auto match = code.matchFirst(expr); + + string arg1 = match[1]; + string arg2 = match[2]; + string arg3 = match[3]; + string arg4 = match[4]; + + auto exprArg = regex(`^(FAUSTFLOAT|int)\s*\*`); + arg1 = arg1.replaceFirst(exprArg, "$1 const*"); + arg3 = arg3.replaceFirst(exprArg, "$1 const*"); + arg4 = arg4.replaceFirst(exprArg, "$1 const*"); + + ulong start = match[0].ptr - code.ptr; + ulong end = start + match[0].length; + code = format("%svoid compute(%s, %s, %s, %s)%s", code[0..start], arg1, arg2, arg3, arg4, code[end..$]); + } + + return code; +} + +string addParameters(string code, Parameter[] params) +{ + string[] addedLines; + + foreach (Parameter param; params) + { + string camelName = param.name.camelify; + addedLines ~= ""; + addedLines ~= format(` FAUSTFLOAT get%s() const { return %s; }`, camelName, param.var); + if (!param.readonly) + addedLines ~= format(` void set%s(FAUSTFLOAT value) { %s = value; }`, camelName, param.var); + } + + return addToClass(code, addedLines.join('\n')); +} + +string addMethodStartEnd(string code, string method) +{ + string[] newLines; + newLines.reserve(1024); + + auto expr = regex(`^(\s*).*\b` ~ method.escaper.to!string ~ `\s*\(.*\{\s*$`); + + bool inMethod = false; + string eom = null; + + foreach (string line; code.lineSplitter) + { + if (inMethod) + { + if (line.stripRight == eom) + { + newLines ~= "\t\t//[End:" ~ method ~ "]"; + inMethod = false; + } + newLines ~= line; + } + else + { + newLines ~= line; + auto match = line.matchFirst(expr); + if (match) + { + inMethod = true; + eom = match[1] ~ '}'; + newLines ~= "\t\t//[Begin:" ~ method ~ "]"; + } + } + } + + return newLines.join('\n'); +} + +string addClassStartEnd(string code) +{ + { + auto expr = regex(`^class\b.*$`, "m"); + auto match = code.matchFirst(expr); + ulong start = match[0].ptr - code.ptr; + ulong end = start + match[0].length; + code = code[0..start] ~ "\n//[Before:class]\n" ~ match[0] ~ "\n\t//[Begin:class]\n" ~ code[end..$]; + } + + { + auto expr = regex(`^\};`, "m"); + auto match = code.matchLast(expr); + ulong start = match[0].ptr - code.ptr; + ulong end = start + match[0].length; + code = code[0..start] ~ "\n\t//[End:class]\n" ~ match[0] ~ "\n//[After:class]\n" ~ code[end..$]; + } + + return code; +} + +string addToClass(string code, string addend) +{ + if (addend.empty) + return code; + + auto expr = regex(`^\};`, "m"); + auto match = code.matchLast(expr); + ulong index = match[0].ptr - code.ptr; + return code[0..index] ~ addend ~ '\n' ~ code[index..$]; +} + +string camelify(string name) +{ + dchar[] result; + result.reserve(name.length); + + bool isIdentifierChar(dchar ch) + { + return std.ascii.isAlphaNum(ch) || ch == '_'; + } + + dchar[] temp = name.to!(dchar[]); + + foreach (ref dchar uniChar; temp) + { + uniChar = [uniChar].normalize!NFD[0]; + if (!isIdentifierChar(uniChar)) + uniChar = ' '; + } + + foreach (dchar[] part; temp.split(' ')) + { + if (!part.empty) + part[0] = std.uni.toUpper(part[0]); + result ~= part; + } + + return result.to!string; +} + +Captures!S matchLast(S, R)(S input, R expr) +{ + Captures!S last; + foreach (Captures!S current; input.matchAll(expr)) + last = current; + return last; +} + +string getFaust() +{ + char *env = getenv("FAUST"); + return env ? env.to!string : "faust"; +} diff --git a/src/sfizz/dsp/platform.lib b/src/sfizz/dsp/platform.lib new file mode 100644 index 00000000..9cd53a3f --- /dev/null +++ b/src/sfizz/dsp/platform.lib @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------------- +// A version of platform.lib which does not limit the sample rate. +// This allows use of high oversampling factors. +//----------------------------------------------------------------------------- + +declare name "sfizz Generic Platform Library"; +declare license "BSD-2-Clause"; + +//---------------------------------`(pl.)SR`----------------------------------- +// Current sampling rate (between 1Hz and 192000Hz). Constant during +// program execution. +//----------------------------------------------------------------------------- +SR = fconstant(int fSamplingFreq, ); + +//---------------------------------`(pl.)tablesize`---------------------------- +// Oscillator table size +//----------------------------------------------------------------------------- +tablesize = 1 << 16;