goofy2k / ESP32_faust2api

Towards a faust2api for ESP32
6 stars 0 forks source link

ESP32 with Faust

Exploring Digital Sound Processing with Faust on a TTGO TAudio board

Objective

[Faust]() is a system for Digital Sound Processing (creating digitally synthesized audio).

It takes a sound engine (.dsp) file generated in an GUI or with a text editor and generates C++ code that can be included in a user's project for creation of firmware for sound generating application.

workflow_partial

Recently Faust has been used for an ESP32 based board: [Lilygo TTGO TAudio](). See this article for genneral information on the capabilities of Faust on the TTGO TAudio board and this tutorial that contains walktrough examples using either the ESP-IDF environment (cli) or the Arduino IDE.

More:

Project flow

The faust2api script is a bash script and requires a Faust environment under Linux. Setting up this environment is considered treated as a separate project, see the linux4faust repo.

For maximum flexibility in firmware generation (using the external RAM for large size firmware) the ESP-IDF environment is preferred over the Arduino IDE. Current potential solutions in my linux4faust project do not allow usage of serial ports. So until now firmware flashing requires ESP-IDF under Windows.

For validation and testing of the Linux environment for Faust preferably in combination with the ESP-IDF environment , a proven set with sound engine (.dsp) and ESP32 user app, workflow must be available.

Milestones

  1. Faust IDE under Windows: step through the walkthrough in the Faust for ESP32 tutorial activity OBSOLETE, we can use Faust scripts now
    a. ESP-IDF
    b. external RAM with ESP-IDF
    c. Arduino IDE
    d. check if external RAM with Arduino IDE is possible
  2. faust2esp32 script under Linux (use master-dev branch with ESP32 entry in faust2api, also requires success in linux4faust) a. Arduino IDE
    b. ESP-IDF
    c. external RAM with ESP-IDF
    d. ONLY if necessary use Arduino IDE for testing
  3. faust2api script under Linux (use master-dev branch with ESP32 entry in faust2api, also requires success in linux4faust)
    a. ESP-IDF
    b. external RAM with ESP-IDF
    c. ONLY if necessary use Arduino IDE for testing
  4. define the roadmap to polyphony on ESP32

As we can run Linux scripts now, milestone 1 does not add anything. The Faust IDE is useful for checking audio output of .dsp files.

TODO

# milestone desrciption DSP Faust script depends on status
1 * design command line scripts started
2 2a Arduino sketch FaustSawtooth.dsp faust2esp32 finished
3 2b ESP-IDF tutorial_app1 FaustSawtooth.dsp faust2esp32 3 finished
4 2c ESP-IDF tutorial_app2 osc.dsp/ext RAM faust2esp32 4 w
4 2c ESP-IDF tutorial_app2 FaustSawtooth.dsp faust2api build07
5 3 ESP-IDF proposed app osc.dsp/ext RAM faust2esp32 4 w
6 4a study and evaluate API documentation faust2api w
8 4b ESP-IDF app with implemented API FaustSawtooth.dsp faust2api w
8 5a ESP-IDF app with implemented API osc.dsp/ext RAM faust2api 5,6 w
9 5b run sound generation tests with API osc.dsp/ext RAM faust2api 7 w

Activities 6,7,8 are the core and will run in improvement cycles
proposed app:

User experience vs architecture

Milestone 2a/b Follow the tutorial workflow, see:

~$ faust2esp32 -lib /mnt/c/Users/Fred/esp_projects/ESP32_faust2api/sound_engines/faust2api/FaustSawtooth/FaustSawtooth.dsp no further output on screen

C++ code

Tool learnings

ESP-IDF

Must add .cpp lib files to CMakeLists.txt in main folder

ESP-IDF 4.2 Powershell from project folder (parent folder of main folder)
PS idf.py set-target -esp32
PS idf.py menuconfig
PS idf.py --no-ccache build
PS idf.py --no-ccache -p COM10 flash
PS idf.py --no-ccache -p COM10 monitor

Use --no-cchache option to prevent build errors with long paths

Arduino IDE

Lab log

Milestone 7
FaustSawtooth and faust2api
doesn't compile may have something to do with long path lengths, see: https://www.esp32.com/viewtopic.php?t=14651
and informative link therein: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/build-system.html#idf-py-options possible workaround: One way to workaround it is to set env variable ..... IDF_CCACHE_ENABLED='' .... before launching idf.py. SET
$Env: = ""
GET

NO SOLUTION !!!

READ THIS: https://www.cxyzjd.com/article/zhangjingxun12/117095349

has something to do with paths > 90

idf.py --no-ccache build

Now the error becomes: undefined reference to Libname::function name

In: https://stackoverflow.com/questions/1517138/trying-to-include-a-library-but-keep-getting-undefined-reference-to-messages

The trick here is to put the library AFTER the module you are compiling. The problem is a reference thing. The linker resolves references in order, so when the library is BEFORE the module being compiled, the linker gets confused and does not think that any of the functions in the library are needed. By putting the library AFTER the module, the references to the library in the module are resolved by the linker.

BUT HOW AND WHERE?

https://stackoverflow.com/questions/67039814/linker-error-in-esp-idf-framework-undefined-reference UPDATE THE CMAKE fil

SO ESP-IDF: set env variable CCACH + updated CMAKELists and use nocach in idf.py -nocahce build (IS THE ENV VAR REQUIRED? )

Arduino: 'dynamic_cast' not permitted with -fno-rtti TRY TO DETECT WHERE THE ERROR OCCURS BY COMMENTING OUT CALLS TO THE LIB

It is still there!

sletz: needs to remove the compilation flag -fno-rtti and add -fexception, but I'm not sure you can do that

the other error should be solved: sletz: This error "error: 'cerr' is not a member of 'std'" should be fixed in this commit https://github.com/grame-cncm/faust/commit/6275eabbde7bc736c69bf44278bd343d27e90f94
I imported the active files. Hope that everything compiles well CHECK CHECK

CHeck this by comparing the uploaded buildlog.txt with the newly generated buildlog2.txt

The source must be on the windows side!

It is now (?) .... output in buildlog3.txt

NOTE: sletz repaired the output file of the script file manually. Doe it mean that a script-generated file is also OK????

THe cerr is still there! Do you use the correct source? No have rebuilt faust (Linux)

Now get for Arduino:


C:\Users\Fred\esp_projects\ESP32_faust2api\sound_engines\faust2api\FaustSawtooth\arduino\DspFaust.cpp: In member function 'void dsp_voice_group::buildUserInterface(UI*)': DspFaust.cpp:10886:79: error: 'dynamic_cast' not permitted with -fno-rtti

... and for ESP-IDF (see buildlog4.txt):


../main/DspFaust.cpp: In member function 'void dsp_voice_group::buildUserInterface(UI)': ../main/DspFaust.cpp:10886:79: error: 'dynamic_cast' not permitted with -fno-rtti if (!fGroupControl || dynamic_cast<SoundUIInterface>(ui_interface)) { ^

../main/DspFaust.cpp: In constructor 'DspFaust::DspFaust(bool)': ../main/DspFaust.cpp:24963:26: error: exception handling disabled, use -fexceptions to enable throw std::bad_alloc(); ^


Both platforms have a 'dynamic_cast' not permitted with -fno-rtti error Arduino
DspFaust.cpp:10886:79:
DspFaust.cpp:11230:59:
DspFaust.cpp:11231:74:

https://stackoverflow.com/questions/8469900/cant-downcast-because-class-is-not-polymorphic
RTTI = run-time type information (RTTI)
https://stackoverflow.com/questions/4486609/when-can-compiling-c-without-rtti-cause-problems !!!

COMPILER OPTION FOR RTTI: CONFIG_COMPILER_CXX_RTTI where to use ?
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/kconfig.html#config-compiler-cxx-rtti in sdkconfig
set CONFIG_COMPILER_CXX_RTTI=y

NOTE this is removed again after running idf.py set-target esp32 enable it again before running build buildlog5.txt

CONFIG_CXX_EXCEPTIONS=y in sdkconfig https://esp32.com/viewtopic.php?t=8575

make it "hard" with menuconfig? in Compiler options

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/kconfig.html#config-cxx-exceptions

success buildlog7.txt success!!

instantiated AND started lib >runtime error heap error

instantiate only: buildlog8.txt buildlog8.txt command line now looks: as this.

Toggle logging run-time output with ctrl L

functions:

TODO

  1. ONGOING test API calls (see API overview below)

    • put API calls in a loop to enable multiple different calls that repeat
    • polyphony commands do not work (newVoice command gives result 0, no polyphony)
      • create and test sound engine with polyphony enabled
  2. DONE Repair JSONUI failure (Midimeta::analyze)

    • this interacts with activation of polyphony. In the end the solution was increasing stack size
  3. ONGOING External communication (UI), e.g. with:

    • Nodered (via WIFI)

      • keep status of widgets in GUI synchronized with status of parameters on board (see DspFaust, modifyZon, reflectZone
      • if a widget setting is changed, the change should be introduced to the board
      • if the board firmware changes a setting of a parameter, the change should be introduced to the GUI widget(s)
      • how to deal with parameters per voice?
      • differentiate between Faust UI parameters and self defined parameters
      • inititalize specific widgets on GUI startup/reset or on board startup/reset
    • Browser (via USB/serial) MAY NEED tot solve memory issues, e.g. by:

    • setting low requirements for logging (esp-idf reduce firmware size)

    • using a less memory-hungry connection method (e.g wired UART/I2C to peer ESP32 as hub)

    • reconnect wifi or mqtt on disconnect

  4. Use external RAM

  5. Optimize WIFI memory usage

  6. Clean up jdksmidi files

    • from V6.1 the files of the jdksmidi lib (Thomas Hofman hack) have been added to the project
    • because of circular includes in this lib, some files are present more than one time and have been renamed
    • clean this up and put the lib in a separate include folder (see issue #10)
  7. Receiving MIDI messages over MQTT is not particularly real-time. A solution would be to add timestamped MQTT note messages in a buffer and play those shifted real time with the same algorithm to read the UART buffer. Note: this is suitable for a sequencer-like application, but not for real real-time applications.

    • first step: investigate if a midi or note handler is in the DspFaust code (so a code like midi-handler in esp32_midi, but using a different buffer that the one supplied by the UART.
  8. For solving the polyphony hum problem, start a high level audio task: class esp32audio

    • but first, there are two time critical tasks in DspFaust

    • xTaskCreatePinnedToCore(processMidiHandler, "Faust MIDI Task", 4096, (void*)this, 5, &fProcessMidiHandle, 1) == pdPASS so midi is handled by CPU1

    • xTaskCreatePinnedToCore(audioTaskHandler, "Faust DSP Task", 4096, (void*)this, 24, &fHandle, 0) == pdPASS so audio is handled by CPU0

    • see how these interfere with the chosen stratey for WiFi and MQTT (main.cpp or ESP-IDF menuconfig)

    • the issues with polyphony remain when the WIFI and MQTT functionality is switched off !

    • it looks from the code that no additional interrupts or tasks are created when using a second voice.
      the contributions of additional voices are simply computed in sequence and the voices are added before copying a buffer to the audio codec

    • going further into this with logging messages....

    • when playing a sequence "over" a background note, the sequence "hangs" as soon as the newVoice has been called.

    • A hum appears and after some time " esp_timer: timer queue overflow" messages appear.

    • The next note is never played.

    • MQTT messages for changing controls arrive OK

    • changing Dsp parameters like ADRS, waveTravel etc arrive OK AND !!! result into changes in the sound.

    • it looks like Dsp processing goes on more or less OK

    • moving the audio task to CPU 1 helps! Current status: Audio: CPU 1, MIDI (off?) CPU 1 , WIFI CPU 1, MQTT CPU 1

    • move WiFi to core 0: Current status: Audio: CPU 1, MIDI (off?) CPU 1 , WIFI CPU 0, MQTT CPU 1 better??

    • switch to other dsp (elecGuitarMidi)

    • task list in loop, before starting player. (see Github, mriksman / esp-idf-homekit)

Name State Priority High Water Stack Number
DSP Task R 24 3244 18
mqtt_task R 5 4644 17
main R 1 8244 5
IDLE1 R 0 1084 7
IDLE0 R 0 1080 6
tiT (C) B 18 2136 12
Tmr Svc (C) B 1 1636 8
ipc1 B 24 524 3
sys_evt (C) B 20 864 15
esp_timer B 22 3384 1
wifi B 23 1036 16
ipc0 B 24 564 2

High Water Mark is the minimum amount of stack space that has remained for the task since the task was created. The closer this value is to zero the closer the task has come to overflowing its stack.

States are:

R -- Ready
X -- Running (the calling task is querying its own priority)
D -- Deleted (waiting clean up)
B -- Blocked
S -- Suspended, or Blocked without a timeout

Note: (C) means it is configurable by menuconfig.

  1. For creation of alternative MIDI input (non) uart, start at base class in midi.h , derived esp32_midi and have a look at other midi_handlers (teensy_midi , juce_midi_handler, ...). Is it possible to re-use jdsk code?

    • start: look how esp32 midi handler uses the base class in midi.h
    • when I copy the Hofmann files into the main folder and into a jdskmidi folder inside main, all changes in the DspFaust can be reverted !!! The only adaptation is with the throw bad_alloc command.
  2. Upload an RTTTL song via MQTT (flexible ringtone)

  3. Do not accept keyOff command right after startup

external ram options

We now have a working basic example app (faust_mqtt_tcp4_v3_KEEP). The file Basic ESP32 faust2api example.md contains a walkthrough on how to create and use this example.

Any changes implemented during write up of this consolidation are done in faust_mqtt_tcp5_v1

Basic ESP32 faust2api example

Testing some API calls:

encountering problems with creating a note sequence based on the above two procedures

When entering more notes in a sequence the program tends to hang.Strange enough this depends on the debug level. It looks like a timing problem: vTaskDelay is used to define the separation between notes, but this is blocking code. This may cause other parts of the program to fail. A way out may be the use of non-blocking timers. See FreeRTOS.
An additional remark: for polyphony, where notes can overlap, sequencing based on definition of delays between notes is intrinsicly not suitable.
be aware that ESP-IDF FreeRTOS is not the native FreeRTOS. Have a look at: ESP-IDF FreeRTOS SMP Changes (SMP: Symmetric Multi Processing) and FreeRTOS Additions for ESP-IDF. It may be wise to use the ESP-IDF FreeRTOS API description and further documentation rather than the documentation on the FreeRTOS site. Also note that a number of FreeRTOS settings can be configured in ESP-IDF via idf.py menuconfig.

The solution for the hanging code is: do not use vTaskDelay. This is blocking other freeRTOS tasks. For playing a sequence of non-overlapping notes, use the self-defined non-blocking function nbDelay (as of faust_mqtt_tcp5_v1). It was tested in sequence playing procedures ending with _nb and found OK for non-overlapping notes. These routines now play the sequence independent of the logging level!

Using FreeRTOS software timers in ESP-IDF

first error: first error Root cause: timer period of 0 is not allowed. This is related to the timer number (x) in the example. Let x start at 1 OR calulate timer period based on x+1.

NOTE: have a look at a parser/dispatcher implementation in void addGenericZone( . Learn from that for you API implementation. BUT: little info on the web on gsscanf. Be careful.


Faust API (taken from README generated with FaustSawtooth)

This API allows to interact with a Faust object and its associated audio engine at a high level. The idea is that all the audio part of the app is implemented in Faust allowing developers to focus on the design of the application itself.

Application Set-Up

Import DspFaust.h and DspFaust.cpp in your project (this can be done simply by dragging these files in your project tree). Then, import DspFaust.h (#include "DspFaust.h") in the file where you want to create/control the Faust object.

Using the C++ API

The current Faust API is designed to seamlessly integrate to the life cycle of an app. It is accessible through a single DspFaust object. The constructor of that object is used to set the sampling rate and the buffer size:

DspFaust* dspFaust = new DspFaust(SR, BS);

The start() method is used to start the audio computing. Similarly, stop() can be called to stop the audio computing.

It is possible to interact with the different parameters of the Faust object by using the setParamValue method. Two versions of this method exist: one where the parameter can be selected by its address and one where it can be selected using its ID. The Parameters List section gives a list of the addresses and corresponding IDs of the current Faust object.

If your Faust object is polyphonic (e.g. if you used the -nvoices option when generating this API), then you can use the MIDI polyphony methods like keyOn, keyOff, etc.

It is possible to change the parameters of polyphonic voices independently using the setVoiceParamValue method. This method takes as one of its arguments the address to the voice returned by keyOn or newVoice when it is called. E.g:

uintptr_t voiceAddress = dspFaust->keyOn(70, 100);
dspFaust->setVoiceParamValue(1, voiceAddress, 214);
dspFaust->keyOff(70);

In the example above, a new note is created and its parameter ID 1 is modified. This note is then terminated. Note that parameters addresses (path) are different for independent voices than when using setParamValue. The list of these addresses is provided in a separate sub-section of the Parameters List section.

Finally, note that new voices don't necessarily have to be created using keyOn. Indeed, you might choose to just use the newVoice method for that:

uintptr_t voiceAddress = dspFaust->newVoice();
dspFaust->setVoiceParamValue(1, voiceAddress, 214);
dspFaust->deleteVoice(voiceAddress);

This is particularly useful when making applications where each finger of the user is an independent sound that doesn't necessarily has a pitch.

In case you would like to use the built-in accelerometer or gyroscope of your device to control some of the parameters of your Faust object, all you have to do is to send the raw accelerometer data to it by using the propagateAcc or propagateGyr for the gyroscope. After that, mappings can be configured directly from the Faust code using this technique or using the setAccConverter and setGyrConverter method. https://ccrma.stanford.edu/~rmichon/faustTutorials/

Parameters List (taken from README generated with FaustSawtooth)

Main Parameters

API Reference

DspFaust(bool auto_connect = true)

Default constructor, to be used wih audio drivers that impose their sample rate and buffer size (like JACK and JUCE).

Arguments


DspFaust(int SR, int BS, bool auto_connect = true)

Constructor.

Arguments


DspFaust(const string& dsp_content, int SR, int BS, bool auto_connect = true)

Constructor.

Arguments


bool start()

Start the audio processing. :white_check_mark:

Returns true if successful and false if not.


void stop()

Stop the audio processing.


bool isRunning()

Returns true if audio is running. :white_check_mark:


long keyOn(int pitch, int velocity)

Instantiate a new polyphonic voice. This method can

only be used if the [style:poly] metadata is used in

the Faust code or if the -nvoices flag has been

provided before compilation.

keyOn will return 0 if the Faust object is not

polyphonic or the address to the allocated voice as

an uintptr_t otherwise. This value can be used later with

setVoiceParamValue or

getVoiceParamValue to access

the parameters of a specific voice.

Arguments


int keyOff(int pitch)

De-instantiate a polyphonic voice. This method can

only be used if the [style:poly] metadata is used in

the Faust code or if the -nvoices flag has been

provided before compilation.

keyOff will return 0 if the object is not polyphonic

and 1 otherwise.

Arguments

as the one used for keyOn


uintptr_t newVoice()

Instantiate a new polyphonic voice. This method can

only be used if the [style:poly] metadata is used in

the Faust code or if -nvoices flag has been

provided before compilation.

newVoice will return 0 if the Faust object is not

polyphonic or the address to the allocated voice as

a uintptr_t otherwise. This value can be used later with

setVoiceParamValue, getVoiceParamValue or

deleteVoice to access the parameters of a specific

voice.


int deleteVoice(uintptr_t voice)

De-instantiate a polyphonic voice. This method can

only be used if the [style:poly] metadata is used in

the Faust code or if -nvoices flag has been

provided before compilation.

deleteVoice will return 0 if the object is not polyphonic

and 1 otherwise.

Arguments


void allNotesOff(bool hard = false)

Terminates all the active voices, gently (with release when hard = false or immediately when hard = true).


void propagateMidi(int count, double time, int type, int channel, int data1, int data2)

Take a raw MIDI message and propagate it to the Faust

DSP object. This method can be used concurrently with

keyOn and keyOff.

propagateMidi can

only be used if the [style:poly] metadata is used in

the Faust code or if -nvoices flag has been

provided before compilation.

Arguments


const char* getJSONUI()

Returns the JSON description of the UI of the Faust object. :white_check_mark:


const char* getJSONMeta()

Returns the JSON description of the metadata of the Faust object.


void buildUserInterface(UI* ui_interface)

Calls the polyphonic or monophonic buildUserInterface with the ui_interface parameter.

Arguments


int getParamsCount()

Returns the number of parameters of the Faust object.


void setParamValue(const char* address, float value)

Set the value of one of the parameters of the Faust

object in function of its address (path).

Arguments


void setParamValue(int id, float value)

Set the value of one of the parameters of the Faust

object in function of its id.

Arguments


float getParamValue(const char* address)

Returns the value of a parameter in function of its

address (path).

Arguments


float getParamValue(int id)

Returns the value of a parameter in function of its

id.

Arguments


void setVoiceParamValue(const char* address, uintptr_t voice, float value)

Set the value of one of the parameters of the Faust

object in function of its address (path) for a

specific voice.

Arguments

from keyOn


void setVoiceParamValue(int id, uintptr_t voice, float value)

Set the value of one of the parameters of the Faust

object in function of its id for a

specific voice.

Arguments

from keyOn


float getVoiceParamValue(const char* address, uintptr_t voice)

Returns the value of a parameter in function of its

address (path) for a specific voice.

Arguments

from keyOn)


float getVoiceParamValue(int id, uintptr_t voice)

Returns the value of a parameter in function of its

id for a specific voice.

Arguments

from keyOn)


const char* getParamAddress(int id)

Returns the address (path) of a parameter in function

of its ID.

Arguments


const char* getVoiceParamAddress(int id, uintptr_t voice)

Returns the address (path) of a parameter in function

of its ID.

Arguments

from keyOn)


float getParamMin(const char* address)

Returns the minimum value of a parameter in function of

its address (path).

Arguments


float getParamMin(int id)

Returns the minimum value of a parameter in function

of its ID.

Arguments


float getParamMax(const char* address)

Returns the maximum value of a parameter in function of

its address (path).

Arguments


float getParamMax(int id)

Returns the maximum value of a parameter in function

of its ID.

Arguments


float getParamInit(const char* address)

Returns the default value of a parameter in function of

its address (path).

Arguments


float getParamInit(int id)

Returns the default value of a parameter in function

of its ID.

Arguments


const char* getMetadata(const char* address, const char* key)

Returns the metadataof a parameter in function of

its address (path) and the metadata key.

Arguments


const char* getMetadata(int id, const char* key)

Returns the metadataof a parameter in function of

its iD and the metadata key.

Arguments


void propagateAcc(int acc, float v)

Propagate the RAW value of a specific accelerometer

axis to the Faust object.

Arguments


void setAccConverter(int p, int acc, int curve, float amin, float amid, float amax)

Set the conversion curve for the accelerometer. https://ccrma.stanford.edu/~rmichon/faustTutorials/

Arguments


void propagateGyr(int gyr, float v)

Propagate the RAW value of a specific gyroscope

axis to the Faust object.

Arguments


void setGyrConverter(int p, int gyr, int curve, float amin, float amid, float amax)

Set the conversion curve for the gyroscope.

Arguments


float getCPULoad()

Returns the CPU load (between 0 and 1.0).


void configureOSC(int xmit, int inport, int outport, int errport, const char* address)

Change the OSC configuration.

Arguments


bool isOSCOn()

Return OSC Status.


....

THERE IS A PROBLEM IN ARDUINO WITH RELATIVE PATHS FOR LIB INCLUDES

PUT THE LIBS IN THE SAME FOLDER AS THE .INO FILE AND INCLUDE BETWEEN " "

TRY TO USE RELATIVE INCLUDES IN THE C++ CODE

THIS PREVENTS THAT YOU HAVE TO MAINTAIN TWO VERSIONS FOR EACH LIB SYNCED

DRAW THE FOLDER STRUCTURE BEFOR YOU IMPLEMENT A SOLUTION

Further tooling

**logging of script output for easier of line study of results

script command lines
create batch files?
In target folder:
wsl ~/faust/tools/faust2appls/faust2esp32 -h > faust2esp32_help.txt

PS C:\Users\Fred\esp_projects\ESP32-with-Faust\sound_engines\FaustSawtooth\cpp\faust2esp32> wsl ~/faust/tools/faust2appls/faust2esp32 ../../FaustSawtooth.dsp
PS C:\Users\Fred\esp_projects\ESP32-with-Faust\sound_engines\FaustSawtooth\cpp\faust2esp32> wsl ~/faust/tools/faust2appls/faust2esp32 - > faustesp32_help.txt

** folder structure generator https://ascii-tree-generator.com/

folder_structure

sound_engines/..................a sound engine's configuration is defined by: Faust script, .dsp file
├─ faust2api/....................each sound engine is implemented in a cpp_code and an arduino_sketch
│ ├─ DSPTemplate/
│ ├─ FaustSawtooth/
│ │ ├─ FaustSawtooth.dsp
│ │ ├─ arduino_sketch/
│ │ │ ├─ arduino_sketch.ino
│ │ │ ├─ DspFaust.cpp........lib files have to be stored in 2 places (Arduino does not accept relative paths)
│ │ │ ├─ DspFaust.h
│ │ │ ├─ WM8978.cpp
│ │ │ ├─ WM8978.h
│ │ ├─ cpp_code/
│ │ │ ├─ main/
│ │ │ │ ├─ main.cpp
│ │ │ │ ├─ CMake_stuff
│ │ │ │ ├─ DspFaust.cpp....must implement relative path to arduino_sketch folder for the libs to prevent double maintenance (risky version control)
│ │ │ │ ├─ DspFaust.h
│ │ │ │ ├─ WM8978.cpp
│ │ │ │ ├─ WM8978.h
│ │ │ ├─ CMake_stuff
├─ faust2esp32/
│ ├─ DSPTemplate/
│ ├─ FaustSawtooth/
│ ├─ another_dsp/
│ │ ├─ another_dsp.dsp

Also check:

MELODIES IMPLEMENT IN ARDUINO SAWTOOTH

https://www.browncountylibrary.org/wp-content/uploads/2018/03/arduino_piezo.pdf e.g.
int numTones = 10; // the number of tones in the scale
int tones[] = {261, 277, 294, 311, 330, 349, 370, 392, 415, 440}; // the frequency for each tone
// mid C C# D D# E F F# G G# A