From 146c245690edefc6a9147adf431de6314ba730e2 Mon Sep 17 00:00:00 2001 From: David Braun <2096055+DBraun@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:40:31 -0500 Subject: [PATCH 1/5] update to 2023 touchdesigner with python 3.11 --- README.md | 6 +++--- build_macos.sh | 2 +- build_windows.bat | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3a8bce4..f1bd024 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TD-Faust -TD-Faust is an integration of [FAUST](https://faust.grame.fr) (**F**unctional **AU**dio **ST**ream) and [TouchDesigner](https://derivative.ca/). The latest builds are for TouchDesigner 2022.25370 and newer. Older TD-Faust builds can be found in the [Releases](https://github.com/DBraun/TD-Faust/releases). +TD-Faust is an integration of [FAUST](https://faust.grame.fr) (**F**unctional **AU**dio **ST**ream) and [TouchDesigner](https://derivative.ca/). The latest builds are for TouchDesigner 2023.11290 and newer. Older TD-Faust builds can be found in the [Releases](https://github.com/DBraun/TD-Faust/releases). ## Overview @@ -37,7 +37,7 @@ Examples of projects made with TD-Faust can be found [here](https://github.com/D Visit TD-Faust's [Releases](https://github.com/DBraun/TD-Faust/releases) page. Download and unzip the latest Windows version. Copy `TD-Faust.dll` and the `faustlibraries` folder to this repository's `Plugins` folder. Open `TD-Faust.toe` and compile a few examples. -If you need to compile `TD-Faust.dll` yourself, you should first install [Python 3.9](https://www.python.org/downloads/release/python-3910/) to `C:/Python39/` and confirm it's in your system PATH. You'll also need Visual Studio 2022 and CMake. Then open a cmd window to `thirdparty/libsndfile` and run `call download_libfaust.bat`. Then you can open a cmd window to this repo's root directory and run `call build_windows.bat`. +If you need to compile `TD-Faust.dll` yourself, you should first install [Python 3.11](https://www.python.org/downloads/release/python-3117/) to `C:/Python311/` and confirm it's in your system PATH. You'll also need Visual Studio 2022 and CMake. Then open a cmd window to `thirdparty/libsndfile` and run `call download_libfaust.bat`. Then you can open a cmd window to this repo's root directory and run `call build_windows.bat`. ### macOS @@ -53,7 +53,7 @@ TD-Faust is designed for macOS version 11.0 and later. Also, macOS users need to 4. Install requirements with [brew](http://brew.sh/): `brew install autoconf autogen automake flac libogg libtool libvorbis opus mpg123 pkg-config` 5. In a Terminal window, navigate to `thirdparty/libfaust` and run `sh download_libfaust.sh`. 6. In a Terminal Window, export a variable to the TouchDesigner.app to which you'd like to support. For example: `export TOUCHDESIGNER_APP=/Applications/TouchDesigner.app`, assuming this version is a 2022.22650 build or higher. -7. Optional: depending on the Python version associated with the TouchDesigner you intend to use, run `export PYTHONVER=3.9` or `export PYTHONVER=3.11`. +7. Optional: depending on the Python version associated with the TouchDesigner you intend to use, run `export PYTHONVER=3.11` or `export PYTHONVER=3.9`. 8. In the same Terminal window, navigate to the root of this repository and run `sh build_macos.sh` 9. In a Terminal window, navigate to the root of this repository and run `sh build_macos.sh`. 10. Open `TD-Faust.toe` diff --git a/build_macos.sh b/build_macos.sh index 73c6a7b..7b1f9ca 100644 --- a/build_macos.sh +++ b/build_macos.sh @@ -9,7 +9,7 @@ echo Assuming TouchDesigner is located at $TOUCHDESIGNER_APP if [ "$PYTHONVER" == "" ]; then # Guess which Python version TD uses. - export PYTHONVER=3.9 + export PYTHONVER=3.11 fi echo Building for Python $PYTHONVER diff --git a/build_windows.bat b/build_windows.bat index 0aa23fc..8780982 100644 --- a/build_windows.bat +++ b/build_windows.bat @@ -7,7 +7,7 @@ rem rm Plugins/TD-Faust.dll rm build/CMakeCache.txt if "%PYTHONVER%"=="" ( - set PYTHONVER=3.9 + set PYTHONVER=3.11 ) echo "Using Python version: %PYTHONVER%" From 1f11129df2a9f857acadf2427d8b7a18b053c0a1 Mon Sep 17 00:00:00 2001 From: David Braun <2096055+DBraun@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:55:46 -0500 Subject: [PATCH 2/5] add faust2touchdesigner --- .gitignore | 3 +- README.md | 25 ++ faust2td.py | 292 ++++++++++++++++ faust2touchdesigner/CMakeLists.txt | 78 +++++ faust2touchdesigner/template_FaustCHOP.cpp | 381 +++++++++++++++++++++ faust2touchdesigner/template_FaustCHOP.h | 105 ++++++ faust2touchdesigner/template_faustaudio.h | 26 ++ reverb.dsp | 3 + 8 files changed, 912 insertions(+), 1 deletion(-) create mode 100644 faust2td.py create mode 100644 faust2touchdesigner/CMakeLists.txt create mode 100644 faust2touchdesigner/template_FaustCHOP.cpp create mode 100644 faust2touchdesigner/template_FaustCHOP.h create mode 100644 faust2touchdesigner/template_faustaudio.h create mode 100644 reverb.dsp diff --git a/.gitignore b/.gitignore index 1329204..2531337 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,10 @@ CrashAutoSave* Backup/* Release/ Debug/ -build/* +build* TD-Faust.[0-9]*.toe SketchSynth.[0-9]*.toe +*dsp.json # Prerequisites *.d diff --git a/README.md b/README.md index f1bd024..840a3f0 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,31 @@ Demo / Tutorial: Examples of projects made with TD-Faust can be found [here](https://github.com/DBraun/TD-Faust/wiki/Made-With-TD-Faust). Contributions are welcome! +## Building a Custom Operator + +The previous overview was about using a multi-purpose CHOP to dynamically compile Faust code inside TouchDesigner. Although it's powerful, you have to specify the DSP code, press the `compile` parameter, and then begin modifying the CHOP's parameters. Ordinary CHOPs can be created from the Op Create Dialog and don't require nearly as much setup, but they are narrower in purpose. What if you want to use Faust to create a more single-purpose CHOP such as a dedicated Reverb CHOP? In this case, you should use the `faust2touchdesigner.py` script. + +These are the requirements: + +* Download libfaust by going to `thirdparty/libfaust` and running either `call download_libfaust.bat` on Windows or `sh download_libfaust.sh` on macOS. +* Pick a Faust DSP file such as `reverb.dsp` that defines a `process = ...;`. +* Python should be installed. +* CMake should be installed. + +If on Windows, you should open an x64 Native Tools for Visual Studio command prompt. On macOS, you can use Terminal. Then run a variation of the following script: + +```bash +python faust2td.py --dsp reverb.dsp --type "Reverb" --label "Reverb" --icon "Rev" --author "David Braun" --email "github.com/DBraun" --drop-prefix +``` + +Limitations and Gotchas: +* The example script above overwrites `Faust_Reverb_CHOP.h`, `Faust_Reverb_CHOP.cpp`, and `Reverb.h`, so avoid changing those files later. +* Polyphonic instruments have not been implemented. +* MIDI has not been implemented. +* The `soundfile` primitive has not been implemented (`waveform` is ok!) +* CHOP Parameters are not "smoothed" automatically, so you may want to put `si.smoo` after each `hslider`/`vslider`. +* File a GitHub issue with any other problems or requests. Pull requests are welcome too! + ## New to FAUST? * Browse the suggested [Documentation and Resources](https://github.com/grame-cncm/faust#documentation-and-resources). diff --git a/faust2td.py b/faust2td.py new file mode 100644 index 0000000..5c2dd07 --- /dev/null +++ b/faust2td.py @@ -0,0 +1,292 @@ +import json +import re +import math +import argparse +from os.path import isfile, isdir, abspath +import subprocess +import shlex +import platform + + +def parse_ui(items, labels=[]) -> None: + global ui_leaf_items + for item in items: + if 'address' in item: + if drop_prefix and len(labels): + labels = labels[1:] + labels.append(item['label']) + item['label'] = '/'.join(labels) + ui_leaf_items.append(item) + if 'items' in item: + labels.append(item['label']) + parse_ui(item['items'], labels=labels) + + +def item_to_td_parname(item) -> str: + address = item['address'] + if address.startswith('/'): + address = address[1:] + + address = address.split('/') + if len(address) > 1 and drop_prefix: + address = address[1:] + address = '/'.join(address) + + address = re.sub('[^a-zA-Z0-9]', '', address) + + address = address[0].upper() + address[1:].lower() + + return address + + +def add_par_double(item) -> str: + parname = item_to_td_parname(item) + label = item['label'].replace('"', '\\"') + init = item['init'] + min_val = item['min'] + max_val = item['max'] + text = f""" +{{ + OP_NumericParameter np; + + np.name = "{parname}"; + np.label = "{label}"; + np.defaultValues[0] = {init}; + np.minSliders[0] = np.minValues[0] = {min_val}; + np.maxSliders[0] = np.maxValues[0] = {max_val}; + np.clampMins[0] = np.clampMaxes[0] = true; + + OP_ParAppendResult res = manager->appendFloat(np); + assert(res == OP_ParAppendResult::Success); +}}""" + return text + + +def legalName(text) -> str: + # todo: do what the official tdu python module does. + text = text.replace(' ', '_') + return text + + +def add_nentry(item) -> str: + parname = item_to_td_parname(item) + label = item['label'].replace('"', '\\"') + init = item['init'] + theMin = item['min'] + theMax = item['max'] + step = item['step'] + + numItems = math.floor((theMax - theMin) / step) + 1 + + items = [min((theMin + step * i), theMax) for i in range(numItems)] + n = len(items) + + names = labels = [str(i) for i in items] + + for meta in (item['meta'] if 'meta' in item else []): + if 'style' in meta: + style = meta['style'] + + if 'menu' in style and item['type'] == 'nentry': + # style might be "menu{'Noise':0;'Sawtooth':1}" + """ + import("stdfaust.lib"); + s = nentry("Signal[style:menu{'Noise':0;'Sawtooth':1}]",0,0,1,1); + process = select2(s,no.noise,os.sawtooth(440)) * .01 <: _, _; + """ + # https://github.com/Fr0stbyteR/faust-ui/blob/5da18109241d9c0d44974c9afac402809a3c2995/src/components/Group.ts#L27 + reg = re.compile(r"(?:(?:'|_)(.+?)(?:'|_):([-+]?[0-9]*\.?[0-9]+?))") + matches = reg.findall(style) + # matches == [('Noise', '0'), ('Sawtooth', '1'), ('Triangle', '2')] + names = [legalName(pair[0]) for pair in matches] + labels = [pair[0] for pair in matches] + + names = ",".join([f'"{name}"' for name in names]) + labels = ",".join([f'"{label}"' for label in labels]) + + text = f""" +{{ + OP_StringParameter sp; + + sp.name = "{parname}"; + sp.label = "{label}"; + + sp.defaultValue = "{init}"; + + const char *names[] = {{{names}}}; + const char *labels[] = {{{labels}}}; + + OP_ParAppendResult res = manager->appendMenu(sp, {n}, names, labels); + assert(res == OP_ParAppendResult::Success); +}}""" + return text + + +def add_toggle(item) -> str: + + parname = item_to_td_parname(item) + label = item['label'].replace('"', '\\"') + + text = f""" +{{ + OP_NumericParameter np; + + np.name = "{parname}"; + np.label = "{label}"; + + OP_ParAppendResult res = manager->appendToggle(np); + assert(res == OP_ParAppendResult::Success); +}}""" + return text + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser() + parser.add_argument('--dsp', required=True, help="The path (relative or absolute) to a text file containing Faust DSP code.") + parser.add_argument('--type', required=True, help='The unique name for this CHOP. It must start with a ' + 'capital A-Z character, and all the following characters must lower case or numbers (a-z, 0-9)') + parser.add_argument('--label', required=True, help='The text that will show up in the OP Create Dialog.') + parser.add_argument('--icon', required=True, help="This should be three letters (upper or lower case), or numbers, which" + " are used to create an icon for this Custom OP.") + parser.add_argument('--author', required=False, default='', help="The author's name.") + parser.add_argument('--email', required=False, default='', help="The author's email") + parser.add_argument('--drop-prefix', required=False, action='store_true', default=False, + help="Automatically drop the first group name to make the CHOP's parameter names shorter.") + + args = parser.parse_args() + + op_type = args.type # {OP_TYPE} + op_label = args.label # {OP_LABEL} + op_icon = args.icon # {OP_ICON} + author_name = args.author # {AUTHOR_NAME} + author_email = args.email # {AUTHOR_EMAIL} + drop_prefix = args.drop_prefix + + op_type = re.sub('[^a-zA-Z0-9]', '', op_type) + op_type = op_type[0].upper() + op_type[1:] + + op_icon = re.sub('[^a-zA-Z0-9]', '', op_icon) + + assert len(op_icon) == 3, "The OP icon must be three letters or numbers." + + dsp_file = args.dsp + assert isfile(dsp_file), f'The requested DSP file "{dsp_file}" was not found.' + + # Turn the Faust code into C++ code: + faust_script = f'faust -i "{dsp_file}" -lang cpp -cn FaustDSP -json -a faust2touchdesigner/template_faustaudio.h -o faust2touchdesigner/{op_type}.h' + + subprocess.call(shlex.split(faust_script)) + + assert isfile(f'faust2touchdesigner/{op_type}.h') + + json_file = dsp_file + '.json' + assert isfile(json_file), f"The JSON file wasn't found at {json_file}" + + with open(json_file, 'r') as f: + text = f.readlines() + + # hacky thing to fix invalid json + text = '\n'.join([line for line in text if '"library_list":' not in line and '"include_pathnames":' not in line]) + + j = json.loads(text) + + # input widget types are ones which will need custom parameters on a Base COMP. + INPUT_WIDGET_TYPES = ['button', 'checkbox', 'nentry', 'hslider', 'vslider'] + OUTPUT_WIDGET_TYPES = ['hbargraph', 'vbargraph'] + GROUP_WIDGET_TYPES = ['hgroup', 'vgroup', 'tgroup'] + + ui_leaf_items = [] + + parse_ui(j['ui']) + + # remove duplicate addresses + addresses = set() + ui_leaf_items_copy = [] + for item in ui_leaf_items: + if item['address'] not in addresses: + addresses.add(item['address']) + ui_leaf_items_copy.append(item) + + ui_leaf_items = ui_leaf_items_copy + del ui_leaf_items_copy + + set_par_values = [] + + for item in ui_leaf_items: + address = item['address'] + parname = item_to_td_parname(item) + widgettype = item['type'] + if widgettype in ['hslider', 'vslider']: + set_par_values.append(f'm_ui.setParamValue("{address}", inputs->getParDouble("{parname}"));') + elif widgettype in ['checkbox', 'button']: + set_par_values.append(f'm_ui.setParamValue("{address}", inputs->getParInt("{parname}"));') + elif widgettype == 'nentry': + set_par_values.append(f'm_ui.setParamValue("{address}", inputs->getParInt("{parname}"));') + elif widgettype in OUTPUT_WIDGET_TYPES: + pass + else: + raise ValueError(f"Unknown widget type: {widgettype}") + + set_par_values = '\n'.join(set_par_values) + + setup_parameters = [] + + for item in ui_leaf_items: + widgettype = item['type'] + if widgettype in ['hslider', 'vslider']: + setup_parameters.append(add_par_double(item)) + elif widgettype == 'button': + setup_parameters.append(add_toggle(item)) + elif widgettype == 'checkbox': + setup_parameters.append(add_toggle(item)) + elif widgettype == 'nentry': + setup_parameters.append(add_nentry(item)) + elif widgettype in OUTPUT_WIDGET_TYPES: + # todo: automatically build a UI for the user? + pass + else: + raise ValueError(f"Unkown ui widget type: {widgettype}") + + setup_parameters = '\n'.join(setup_parameters) + + with open('faust2touchdesigner/template_FaustCHOP.h', 'r') as f: + template = f.read() + template = template.replace('{OP_TYPE}', op_type) + with open(f'faust2touchdesigner/Faust_{op_type}_CHOP.h', 'w') as f: + f.write(template) + + with open('faust2touchdesigner/template_FaustCHOP.cpp', 'r') as f: + template = f.read() + + template = template.replace('{OP_TYPE}', op_type) + template = template.replace('{OP_LABEL}', op_label) + template = template.replace('{OP_ICON}', op_icon) + template = template.replace('{AUTHOR_NAME}', author_name) + template = template.replace('{AUTHOR_EMAIL}', author_email) + template = template.replace('{SET_PAR_VALUES}', set_par_values) + template = template.replace('{SETUP_PARAMETERS}', setup_parameters) + + with open(f'faust2touchdesigner/Faust_{op_type}_CHOP.cpp', 'w') as f: + f.write(template) + + if platform.system() == 'Windows': + libfaust_dir = 'thirdparty/libfaust/win64/Release' + else: + if 'arm' in platform.processor(): + libfaust_dir = 'thirdparty/libfaust/darwin-arm64/Release' + else: + libfaust_dir = 'thirdparty/libfaust/darwin-x64/Release' + + libfaust_dir = str(abspath(libfaust_dir)) + + if platform.system() == 'Windows': + assert isdir(libfaust_dir), "Have you run `call download_libfaust.bat`?" + else: + assert isdir(libfaust_dir), "Have you run `sh download_libfaust.sh`?" + + # execute CMake and build + subprocess.call(shlex.split(f'cmake faust2touchdesigner -Bbuild_faust2touchdesigner -DOP_TYPE={op_type} -DAUTHOR_NAME="{author_name}" -DLIBFAUST_DIR="{libfaust_dir}" -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0')) + subprocess.call(shlex.split(f'cmake --build build_faust2touchdesigner --config Release')) + + print('All done!') diff --git a/faust2touchdesigner/CMakeLists.txt b/faust2touchdesigner/CMakeLists.txt new file mode 100644 index 0000000..3e92ad8 --- /dev/null +++ b/faust2touchdesigner/CMakeLists.txt @@ -0,0 +1,78 @@ +cmake_minimum_required(VERSION 3.13.0 FATAL_ERROR) + +set(VERSION 0.0.1) +project(${OP_TYPE} VERSION ${VERSION}) + +set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${OP_TYPE}) + +set(TOUCHDESIGNER_INC ${PROJECT_SOURCE_DIR}/../thirdparty/TouchDesigner/) + +set(Headers + "${TOUCHDESIGNER_INC}/CHOP_CPlusPlusBase.h" + "${TOUCHDESIGNER_INC}/CPlusPlus_Common.h" + "${TOUCHDESIGNER_INC}/GL_Extensions.h" + "${PROJECT_SOURCE_DIR}/Faust_${OP_TYPE}_CHOP.h" + "${PROJECT_SOURCE_DIR}/${OP_TYPE}.h" +) +source_group("Headers" FILES ${Headers}) + +set(Sources + "${PROJECT_SOURCE_DIR}/Faust_${OP_TYPE}_CHOP.cpp" +) + +source_group("Sources" FILES ${Sources}) + +set(ALL_FILES + ${Headers} + ${Sources} +) + +add_library(${OP_TYPE} MODULE ${ALL_FILES}) + +set(ROOT_NAMESPACE ${PROJECT_NAME}) + +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 17 + OUTPUT_DIRECTORY_DEBUG "${CMAKE_SOURCE_DIR}/$/" + OUTPUT_DIRECTORY_RELEASE "${CMAKE_SOURCE_DIR}/$/" + INTERPROCEDURAL_OPTIMIZATION_RELEASE "TRUE" + BUNDLE true + BUNDLE_EXTENSION "plugin" + PRODUCT_BUNDLE_IDENTIFIER design.dirt.cpp.${PROJECT_NAME} + MACOSX_BUNDLE_GUI_IDENTIFIER design.dirt.cpp.${PROJECT_NAME} + MACOSX_BUNDLE_INFO_STRING ${PROJECT_NAME} + MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME} + MACOSX_BUNDLE_BUNDLE_VERSION "${VERSION}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${VERSION}" + MACOSX_BUNDLE_COPYRIGHT "${AUTHOR_NAME}" + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Faust2TouchDesigner/Info.plist + XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "/System/Library/PrivateFrameworks /Library/Frameworks" +) + +include_directories(${LIBFAUST_DIR}/include) +include_directories(${LIBFAUST_DIR}/include/faust/architecture) +include_directories(${LIBFAUST_DIR}/include/faust/compiler) +include_directories(${LIBFAUST_DIR}/include/faust/compiler/utils) +include_directories(${TOUCHDESIGNER_INC}) + +target_compile_definitions(${PROJECT_NAME} PRIVATE "OP_TYPE=${OP_TYPE}") + +# Platform-specific libraries and definitions +if(APPLE) + target_compile_definitions(${PROJECT_NAME} PRIVATE "__APPLE__") + # target_link_libraries(${PROJECT_NAME} PRIVATE "-framework CoreFoundation" "-framework CoreMIDI" "-framework CoreAudio") +elseif(MSVC) + # win sock 32; windows multimedia for rt midi + # target_link_libraries(${PROJECT_NAME} PRIVATE winmm ws2_32) + target_compile_definitions(${PROJECT_NAME} PRIVATE "WIN32;_WIN32;_WINDOWS;__WINDOWS_DS__;") +endif() + +if(MSVC) + set_target_properties(${PROJECT_NAME} PROPERTIES + VS_DEBUGGER_COMMAND "C:\\Program Files\\Derivative\\TouchDesigner\\bin\\TouchDesigner.exe" + VS_DEBUGGER_COMMAND_ARGUMENTS "..\\$(ProjectName).toe") + + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND + ${CMAKE_COMMAND} -E copy_if_different "$" ${CMAKE_SOURCE_DIR}/../Plugins + ) +endif() diff --git a/faust2touchdesigner/template_FaustCHOP.cpp b/faust2touchdesigner/template_FaustCHOP.cpp new file mode 100644 index 0000000..739e335 --- /dev/null +++ b/faust2touchdesigner/template_FaustCHOP.cpp @@ -0,0 +1,381 @@ +/* Shared Use License: This file is owned by Derivative Inc. (Derivative) + * and can only be used, and/or modified for use, in conjunction with + * Derivative's TouchDesigner software, and only if you are a licensee who has + * accepted Derivative's TouchDesigner license or assignment agreement + * (which also govern the use of this file). You may share or redistribute + * a modified version of this file provided the following conditions are met: + * + * 1. The shared file or redistribution must retain the information set out + * above and this list of conditions. + * 2. Derivative's name (Derivative Inc.) or its trademarks may not be used + * to endorse or promote products derived from this file without specific + * prior written permission from Derivative. + */ + +#include "Faust_{OP_TYPE}_CHOP.h" + +// general includes +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +using namespace std; + +#ifndef SAFE_DELETE +#define SAFE_DELETE(x) \ + do { \ + if (x) { \ + delete x; \ + x = NULL; \ + } \ + } while (0) +#define SAFE_DELETE_ARRAY(x) \ + do { \ + if (x) { \ + delete[] x; \ + x = NULL; \ + } \ + } while (0) +#endif + +// These functions are basic C function, which the DLL loader can find +// much easier than finding a C++ Class. +// The DLLEXPORT prefix is needed so the compile exports these functions from +// the .dll you are creating +extern "C" { + +DLLEXPORT +void FillCHOPPluginInfo(CHOP_PluginInfo* info) { + // Always set this to CHOPCPlusPlusAPIVersion. + info->apiVersion = CHOPCPlusPlusAPIVersion; + + // The opType is the unique name for this CHOP. It must start with a + // capital A-Z character, and all the following characters must lower case + // or numbers (a-z, 0-9) + info->customOPInfo.opType->setString("{OP_TYPE}"); + + // The opLabel is the text that will show up in the OP Create Dialog + info->customOPInfo.opLabel->setString("{OP_LABEL}"); + info->customOPInfo.opIcon->setString("{OP_ICON}"); + + // Information about the author of this OP + info->customOPInfo.authorName->setString("{AUTHOR_NAME}"); + info->customOPInfo.authorEmail->setString("{AUTHOR_EMAIL}"); + + info->customOPInfo.minInputs = 0; + info->customOPInfo.maxInputs = 1; + + // info->customOPInfo.pythonVersion->setString(PY_VERSION); + // info->customOPInfo.pythonMethods = methods; + // info->customOPInfo.pythonGetSets = getSets; // todo: +} + +DLLEXPORT +CHOP_CPlusPlusBase* CreateCHOPInstance(const OP_NodeInfo* info) { + // Return a new instance of your class every time this is called. + // It will be called once per CHOP that is using the .dll + return new FaustCHOP(info); +} + +DLLEXPORT +void DestroyCHOPInstance(CHOP_CPlusPlusBase* instance) { + // Delete the instance here, this will be called when + // Touch is shutting down, when the CHOP using that instance is deleted, or + // if the CHOP loads a different DLL + delete (FaustCHOP*)instance; +} +}; + +FaustCHOP::FaustCHOP(const OP_NodeInfo* info) : m_NodeInfo(info) { + // sample rate + m_srate = 44100.; // will be written immediately by getOutputInfo + + m_dsp.init(m_srate); + m_dsp.buildUserInterface(&m_ui); // todo: + + // zero + m_input = NULL; + m_output = NULL; + // default + m_numInputChannels = m_dsp.getNumInputs(); + m_numOutputChannels = m_dsp.getNumOutputs(); + + this->allocate(m_numInputChannels, m_numOutputChannels, m_blockSize); +} + +FaustCHOP::~FaustCHOP() { + clearBufs(); +} + +void FaustCHOP::getGeneralInfo(CHOP_GeneralInfo* ginfo, const OP_Inputs* inputs, + void* reserved1) { + // This will cause the node to cook every frame + ginfo->cookEveryFrameIfAsked = true; + + // Note: To disable timeslicing you'll need to turn this off, as well as + // ensure that getOutputInfo() returns true, and likely also set the + // info->numSamples to how many samples you want to generate for this CHOP. + // Otherwise it'll take on length of the input CHOP, which may be timesliced. + ginfo->timeslice = true; +} + +bool FaustCHOP::getOutputInfo(CHOP_OutputInfo* info, const OP_Inputs* inputs, + void* reserved1) { + // If there is an input connected, we are going to match it's channel names + // etc otherwise we'll specify our own. + + info->numChannels = m_numOutputChannels; + + // Since we are outputting a timeslice, the system will dictate + // the numSamples and startIndex of the CHOP data + // info->numSamples = 1; + // info->startIndex = 0 + + info->sampleRate = std::max(1., inputs->getParDouble("Samplerate")); + + bool needRecompile = m_srate != info->sampleRate; + m_srate = info->sampleRate; + + if (needRecompile) { + m_dsp.init(m_srate); + m_dsp.buildUserInterface(&m_ui); + } + + return true; +} + +void FaustCHOP::getChannelName(int32_t index, OP_String* name, + const OP_Inputs* inputs, void* reserved1) { + std::stringstream ss; + ss << "chan" << (index + 1); + name->setString(ss.str().c_str()); +} + +void FaustCHOP::clearBufs() { + if (m_input != NULL) { + for (int i = 0; i < m_numInputChannels; i++) { + SAFE_DELETE_ARRAY(m_input[i]); + } + } + if (m_output != NULL) { + for (int i = 0; i < m_numOutputChannels; i++) { + SAFE_DELETE_ARRAY(m_output[i]); + } + } + SAFE_DELETE_ARRAY(m_input); + SAFE_DELETE_ARRAY(m_output); + + m_allocatedSamples = 0; +} + +void FaustCHOP::allocate(int inputChannels, int outputChannels, + int numSamples) { + // clear + clearBufs(); + + // set + m_numInputChannels = min(inputChannels, MAX_INPUTS); + m_numOutputChannels = min(outputChannels, MAX_OUTPUTS); + + // allocate channels + m_input = new FAUSTFLOAT*[m_numInputChannels]; + m_output = new FAUSTFLOAT*[m_numOutputChannels]; + m_allocatedSamples = numSamples; + // allocate buffers for each channel + for (int chan = 0; chan < m_numInputChannels; chan++) { + // single sample for each + m_input[chan] = new FAUSTFLOAT[numSamples]; + } + for (int chan = 0; chan < m_numOutputChannels; chan++) { + // single sample for each + m_output[chan] = new FAUSTFLOAT[numSamples]; + } +} + +void FaustCHOP::getWarningString(OP_String* warning, void* reserved1) { + warning->setString(m_warningString.c_str()); +} + +void FaustCHOP::getErrorString(OP_String* error, void* reserved1) { + error->setString(m_errorString.c_str()); +} + +void FaustCHOP::execute(CHOP_Output* output, const OP_Inputs* inputs, + void* reserved) { + m_warningString = std::string(""); + + {SET_PAR_VALUES} + + if (output->numChannels == 0 || output->numChannels != m_numOutputChannels) { + // write zeros and return + for (int chan = 0; chan < output->numChannels; chan++) { + auto writePtr = output->channels[chan]; + memset(writePtr, 0, output->numSamples * sizeof(float)); + } + return; + } + + const OP_CHOPInput* audioInput = inputs->getInputCHOP(0); + + if (!audioInput && m_numInputChannels) { + // write zeros and return + for (int chan = 0; chan < output->numChannels; chan++) { + auto writePtr = output->channels[chan]; + memset(writePtr, 0, output->numSamples * sizeof(float)); + } + m_errorString = std::string("This plugin requires an input CHOP with " + + to_string(m_numInputChannels) + " channels."); + return; + } + + // A reasonably large block size. Code farther below will make it smaller when + // polyphony is necessary, or the control signals are high audio rate. + m_blockSize = 1024; + + if (m_blockSize > m_allocatedSamples) { + allocate(m_numInputChannels, m_numOutputChannels, m_blockSize); + } + + // if channels are expected, but the number of channels provided is less than + // what's needed, make a warning. + if (m_numInputChannels) { + if (!audioInput || audioInput->numChannels < m_numInputChannels) { + m_warningString = + std::string("Not enough audio input channels. Expected " + + to_string(m_numInputChannels) + " but received " + + to_string(audioInput->numChannels)); + } + } + if (audioInput && audioInput->numChannels > m_numInputChannels) { + m_warningString = + std::string("Too many audio input channels: Expected " + + to_string(m_numInputChannels) + " but received " + + to_string(audioInput->numChannels)); + } + + int numSamples = 0; + float* writePtr = nullptr; + float* readPtr = nullptr; + + int chan = 0; + + for (int i = 0; i < output->numSamples; i += m_blockSize) { + numSamples = min(output->numSamples - i, m_blockSize); + + if (audioInput) { + for (chan = 0; chan < min(m_numInputChannels, audioInput->numChannels); + chan++) { + writePtr = m_input[chan]; + readPtr = (float*)audioInput->channelData[chan]; + readPtr += i; + + memcpy(writePtr, readPtr, + max(0, min(numSamples, audioInput->numSamples - i)) * + sizeof(float)); + } + } + // write zero for any remaining channels + for (; chan < m_numInputChannels; chan++) { + writePtr = m_input[chan]; + memset(writePtr, 0, numSamples * sizeof(float)); + } + + // auto start = high_resolution_clock::now(); + + m_dsp.compute(numSamples, m_input, m_output); + + // auto stop = high_resolution_clock::now(); + // m_duration = duration_cast(stop - start); + + for (chan = 0; chan < output->numChannels; chan++) { + writePtr = output->channels[chan]; + writePtr += i; + memcpy(writePtr, m_output[chan], numSamples * sizeof(float)); + } + } + m_errorString = std::string(""); +} + +int32_t FaustCHOP::getNumInfoCHOPChans(void* reserved1) { + // We return the number of channel we want to output to any Info CHOP + // connected to the CHOP. In this example we are just going to send one + // channel. + + int numChans = 2; + + return numChans; +} + +void FaustCHOP::getInfoCHOPChan(int32_t index, OP_InfoCHOPChan* chan, + void* reserved1) { + // This function will be called once for each channel we said we'd want to + // return In this example it'll only be called once. + + if (index == 0) { + chan->name->setString("inputs"); + chan->value = m_numInputChannels; + } else if (index == 1) { + chan->name->setString("block_size"); + chan->value = m_blockSize; + } else { + } +} + +bool FaustCHOP::getInfoDATSize(OP_InfoDATSize* infoSize, void* reserved1) { + infoSize->rows = 0; + infoSize->cols = 2; + // Setting this to false means we'll be assigning values to the table + // one row at a time. True means we'll do it one column at a time. + infoSize->byColumn = false; + return true; +} + +void FaustCHOP::getInfoDATEntries(int32_t index, int32_t nEntries, + OP_InfoDATEntries* entries, void* reserved1) { +// char tempBuffer[4096]; + +// if (index == 0) { +// // Set the value for the first column +// entries->values[0]->setString("foo"); + +// // Set the value for the second column +// #ifdef _WIN32 +// sprintf_s(tempBuffer, "%d", foo); +// #else // macOS +// snprintf(tempBuffer, sizeof(tempBuffer), "%d", foo); +// #endif +// entries->values[1]->setString(tempBuffer); +// } +} + +void FaustCHOP::setupParameters(OP_ParameterManager* manager, void* reserved1) { + // Sample Rate + { + OP_NumericParameter np; + + np.name = "Samplerate"; + np.label = "Sample Rate"; + np.defaultValues[0] = 44100.0; + np.minSliders[0] = np.minValues[0] = .001; + np.maxSliders[0] = np.maxValues[0] = 192000.0; + np.clampMins[0] = np.clampMaxes[0] = true; + + OP_ParAppendResult res = manager->appendFloat(np); + assert(res == OP_ParAppendResult::Success); + } + + {SETUP_PARAMETERS} +} + +void FaustCHOP::pulsePressed(const char* name, void* reserved1) { + // if (!strcmp(name, "Foo")) { + + // } +} diff --git a/faust2touchdesigner/template_FaustCHOP.h b/faust2touchdesigner/template_FaustCHOP.h new file mode 100644 index 0000000..8575f9f --- /dev/null +++ b/faust2touchdesigner/template_FaustCHOP.h @@ -0,0 +1,105 @@ +/* Shared Use License: This file is owned by Derivative Inc. (Derivative) + * and can only be used, and/or modified for use, in conjunction with + * Derivative's TouchDesigner software, and only if you are a licensee who has + * accepted Derivative's TouchDesigner license or assignment agreement + * (which also govern the use of this file). You may share or redistribute + * a modified version of this file provided the following conditions are met: + * + * 1. The shared file or redistribution must retain the information set out + * above and this list of conditions. + * 2. Derivative's name (Derivative Inc.) or its trademarks may not be used + * to endorse or promote products derived from this file without specific + * prior written permission from Derivative. + */ + +#include "CHOP_CPlusPlusBase.h" +using namespace TD; +#include + +//#include +// using namespace std::chrono; + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +#ifndef MAX_INPUTS +#define MAX_INPUTS 16384 +#endif +#ifndef MAX_OUTPUTS +#define MAX_OUTPUTS 16384 +#endif + +#include "{OP_TYPE}.h" + +#include + +using namespace std; + +// To get more help about these functions, look at CHOP_CPlusPlusBase.h +class FaustCHOP : public CHOP_CPlusPlusBase { + public: + FaustCHOP(const OP_NodeInfo* info); + virtual ~FaustCHOP(); + + virtual void getGeneralInfo(CHOP_GeneralInfo*, const OP_Inputs*, + void*) override; + virtual bool getOutputInfo(CHOP_OutputInfo*, const OP_Inputs*, + void*) override; + virtual void getChannelName(int32_t index, OP_String* name, const OP_Inputs*, + void* reserved) override; + + virtual void execute(CHOP_Output*, const OP_Inputs*, void* reserved) override; + + virtual int32_t getNumInfoCHOPChans(void* reserved1) override; + virtual void getInfoCHOPChan(int32_t index, OP_InfoCHOPChan* chan, + void* reserved1) override; + + virtual bool getInfoDATSize(OP_InfoDATSize* infoSize, + void* resereved1) override; + virtual void getInfoDATEntries(int32_t index, int32_t nEntries, + OP_InfoDATEntries* entries, + void* reserved1) override; + + virtual void getWarningString(OP_String* warning, void* reserved1) override; + virtual void getErrorString(OP_String* error, void* reserved1) override; + + virtual void setupParameters(OP_ParameterManager* manager, + void* reserved1) override; + virtual void pulsePressed(const char* name, void* reserved1) override; + + void clearBufs(); + void allocate(int inputChannels, int outputChannels, int numSamples); + + private: + // We don't need to store this pointer, but we do for the example. + // The OP_NodeInfo class store information about the node that's using + // this instance of the class (like its name). + const OP_NodeInfo* m_NodeInfo; + + // sample rate + float m_srate = 44100.; + + // faust compiler error string + string m_errorString = string(""); + string m_warningString = string(""); + string m_name_app = string(""); + + // buffers + FAUSTFLOAT** m_input = nullptr; + FAUSTFLOAT** m_output = nullptr; + + // input and output + int m_numInputChannels = 0; + int m_numOutputChannels = 0; + int m_allocatedSamples = 0; + + // microseconds m_duration; + + // diagnostic vars: + int m_blockSize = 0; + + FaustDSP m_dsp; + + APIUI m_ui; +}; diff --git a/faust2touchdesigner/template_faustaudio.h b/faust2touchdesigner/template_faustaudio.h new file mode 100644 index 0000000..7bd12f1 --- /dev/null +++ b/faust2touchdesigner/template_faustaudio.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#ifndef FAUSTFLOAT +#define FAUSTFLOAT float +#endif + +struct UI; +struct Meta; + +#include +#include +#include +#include +#include +#include + +<> + +<> \ No newline at end of file diff --git a/reverb.dsp b/reverb.dsp new file mode 100644 index 0000000..027f07e --- /dev/null +++ b/reverb.dsp @@ -0,0 +1,3 @@ +import("stdfaust.lib"); +mix = hslider("v:[0] JPrev/h:[0] Mix/wet [style:knob]", 1., 0, 1, .01) : si.smoo; +process = ef.dryWetMixer(mix, dm.jprev_demo); \ No newline at end of file From a0f403500f56822860b7b2ad87cf722e7ff7efc9 Mon Sep 17 00:00:00 2001 From: David Braun <2096055+DBraun@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:11:06 -0500 Subject: [PATCH 3/5] Update all.yml --- .github/workflows/all.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 83d558e..7374a62 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -45,6 +45,10 @@ jobs: env: PYTHONVER: ${{ matrix.python-version}} + - name: Build Reverb operator + shell: cmd + run: python faust2td.py --dsp reverb.dsp --type "Reverb" --label "Reverb" --icon "Rev" --author "David Braun" --email "github.com/DBraun" --drop-prefix + - name: Make distribution run: | mkdir TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }} @@ -91,6 +95,10 @@ jobs: # python3 --version # sh -v build_macos.sh + # - name: Build Reverb operator + # shell: cmd + # run: python faust2td.py --dsp reverb.dsp --type "Reverb" --label "Reverb" --icon "Rev" --author "David Braun" --email "github.com/DBraun" --drop-prefix + # - name: Make distribution # run: | # rm -rf Plugins/faustlibraries/.git From b2e59a88e779bd6a77f443181cf32449395d3345 Mon Sep 17 00:00:00 2001 From: David Braun <2096055+DBraun@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:22:46 -0500 Subject: [PATCH 4/5] Update all.yml --- .github/workflows/all.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 7374a62..3b12a5c 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -47,7 +47,9 @@ jobs: - name: Build Reverb operator shell: cmd - run: python faust2td.py --dsp reverb.dsp --type "Reverb" --label "Reverb" --icon "Rev" --author "David Braun" --email "github.com/DBraun" --drop-prefix + run: | + set PATH=%CD%/thirdparty/libfaust/win64/Release/bin;%PATH% + python faust2td.py --dsp reverb.dsp --type "Reverb" --label "Reverb" --icon "Rev" --author "David Braun" --email "github.com/DBraun" --drop-prefix - name: Make distribution run: | @@ -97,7 +99,9 @@ jobs: # - name: Build Reverb operator # shell: cmd - # run: python faust2td.py --dsp reverb.dsp --type "Reverb" --label "Reverb" --icon "Rev" --author "David Braun" --email "github.com/DBraun" --drop-prefix + # run: | + # export PATH=$PWD/thirdparty/libfaust/darwin-x64/Release/:$PWD/thirdparty/libfaust/darwin-arm64/Release/:$PATH + # python faust2td.py --dsp reverb.dsp --type "Reverb" --label "Reverb" --icon "Rev" --author "David Braun" --email "github.com/DBraun" --drop-prefix # - name: Make distribution # run: | From 70184af41c9f4d87e12ab90eb35d36c2c4387c8e Mon Sep 17 00:00:00 2001 From: David Braun <2096055+DBraun@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:37:23 -0500 Subject: [PATCH 5/5] Update all.yml --- .github/workflows/all.yml | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 3b12a5c..aad2816 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -22,7 +22,7 @@ jobs: submodules: true - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -53,18 +53,13 @@ jobs: - name: Make distribution run: | - mkdir TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }} - move ${{ github.workspace }}/Plugins/TD-Faust.dll TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }} - move ${{ github.workspace }}/Plugins/sndfile.dll TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }} - cp -v -r ${{ github.workspace }}/Plugins/faustlibraries TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }} - Remove-Item -Recurse -Force "TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }}/faustlibraries/.git" - 7z a TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }}.zip ./TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }}/* -r + Remove-Item -Recurse -Force "${{ github.workspace }}/Plugins/faustlibraries/.git" - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }} - path: TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }}.zip + path: Plugins # build-macos: # strategy: @@ -80,7 +75,7 @@ jobs: # submodules: true # - name: Setup Python 3.8 - # uses: actions/setup-python@v2 + # uses: actions/setup-python@v5 # with: # python-version: '3.8' @@ -106,13 +101,12 @@ jobs: # - name: Make distribution # run: | # rm -rf Plugins/faustlibraries/.git - # zip -r TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }}.zip Plugins - # - name: Upload artifact - # uses: actions/upload-artifact@v3 - # with: - # name: TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }} - # path: TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }}.zip + # - name: Upload artifact + # uses: actions/upload-artifact@v4 + # with: + # name: TD-Faust-${{ matrix.name }}-Python${{ matrix.python-major }} + # path: Plugins create-release: if: startsWith(github.ref, 'refs/tags/v')