Getting started with VoidX development

This guide walks you through creating a basic Digital Delay effect for the Mini Stomp-X platform using the VoidX-Library. You’ll have a functional delay effect up and running in minutes, controllable through the companion VoidX-Control app!

Note: The code used in this guide prioritizes readability and serves as a foundational example to introduce you to VoidX-Library development. It is not optimized for performance or intended for production use.

Setting Up Your ESP32 Development Environment

Follow the installation processes for Visual Studio Code (VS Code) and the essential ESP-IDF Visual Studio Code Extension. With these tools in place, you’ll have a comprehensive development environment for working with the entire ESP32 microcontroller (MCU) family.

To confirm your ESP-IDF toolchain installation is functional, you can use the provided “sample project” Follow these steps:

  • Within VS Code, navigate to the “File” menu and select “Open Folder.”
  • Locate your ESP-IDF installation directory.
  • Inside the directory, navigate to the examples/get-started/sample_project folder and select it.

Upon opening the folder, VS Code should automatically recognize it as an ESP-IDF project and display a bottom-bar like this:

  • Select the target device, typically “ESP32-S3 chip (via ESP-PROG)“, and build the project (wrench icon). A successful build confirms a working toolchain.
  • Connect your VoidX device to your PC via USB and select the corresponding COM port (example COM1) from the VS Code bottom bar. Flash the device (thunder icon) or use the flame icon for a combined Build, Flash and Monitor process.

Excellent! The ESP32 toolchain is now configured. Let’s delve into VoidX-Library development!

Get started with VoidX-Library!

  • Download the VoidX-Public-App code from our GitHub repo.
  • Open it in VS Code and build/flash it directly to your Mini Stomp-X platform. Enjoy your first VoidX-powered device!

Encountered build errors? Ensure you’re using the latest stable ESP-IDF version. If the issue persists, feel free to contact us!

The VoidX-Public-App, powered by the VoidX-Library, is a comprehensive platform for building hardware audio applications. By handling core functionalities, it empowers developers to focus on their creative vision. These core functionalities include:

  • Real-time, low-latency audio threading.
  • Hardware drivers for LCD, navigation encoder, status LED, footswitch, MIDI, audio converters, and more.
  • Communication with VoidX-Control via Bluetooth Low Energy, Wi-Fi, or USB using the VoidX-Protocol.
  • Standard event handling and LCD graphics support.
  • Management of system-level information (name, author, logo, Wi-Fi settings, licensing, etc.).

To craft your desired effect, simply:

  • Define a C++ class that extends AudioBlock.
  • Define and initialize your parameters (nodes).
  • Implement custom audio processing by overriding the exec() and compile() methods for DSP code and pre-computations respectively.

To illustrate the development process, let’s create a Digital Delay application!

Let’s code a Digital Delay!

To maintain code clarity and independence, create (if it doesn’t exist yet) a directory called source under the main folder. All your code should reside within this directory, separate from the framework’s components.

Create the C++ class DigitalDelay, consisting of the header file DigitalDelay.hpp and the source file DigitalDelay.cpp within the project’s main/source directory.

Note: Don’t forget to include any new source files in the CMakeLists.txt file located in your project’s main directory.

idf_component_register(SRCS ...
                            ...
                            "main/source/DigitalDelay.cpp"
                            "main.cpp" 
                            )

We will implement a basic digital delay with three parameters:

  • Mix: determines the blend of original and delayed signals.
  • Feedback: determines the amount of delayed signal fed back into the delay line.
  • Time: sets the duration of the delay.

Start by inserting the following code into the DigitalDelay.hpp file:

#pragma once
#include "dsp/AudioBlock.hpp"
#include "NodeFloat.hpp"

class DigitalDelay: public AudioBlock {
private:
    //Parameters
    NodeFloat * mix;
    NodeFloat * feedback; 
    NodeFloat * timeNode;
 
    //Low-level DSP parameters.
    float * delayLine; //Delay circular buffer
    int delayLineWritePointer; //Write index for the delay line.
    float mixDsp; //Blend level
    float feedbackDsp; //Feedback amount
    int delaySamplesDsp; //Delay length in samples
 
public:
    DigitalDelay(Node * parent, Node *root); //Class constructor
    bool compile(); //Inherited from AudioBlock
    static bool exec(DigitalDelay * ptr, float data[SAMPLING_CHANNELS][SAMPLING_FRAME]); //DSP function is a static function
};

Implement the DigitalDelay class constructor and provide method stubs in DigitalDelay.cpp:

#include "DigitalDelay.hpp"
#include "../bsp/MiniStompX.hpp"
#include <malloc.h>
#include "DrawerHome.hpp"
 
/* DELAY_LINE_SIZE: length of the delay buffer */
#define DELAY_LINE_SIZE ((int)(2 * SAMPLING_FREQ)) 
 
/*
parent: application node pointer (typically root/app).
root: root node pointer.
*/
DigitalDelay::DigitalDelay(Node * parent, Node *root) : AudioBlock("Digital Delay", parent, (bool (*)(void*, float [SAMPLING_CHANNELS][SAMPLING_FRAME]))DigitalDelay::exec){
    /* Initialize the MiniStompX hardware interface. */
    /* BSP classes encapsulate hardware interactions, triggering hardware events, and controlling hardware devices. */
    new MiniStompX();
 
    /* Define application metadata: name, author, and logo. */
    NodeItem * name = (NodeItem *)root->pathToNode("root\\sys\\_name"); //Access the node using its predefined path.
    if(name != NULL) name->setValue("Digital Delay"); //Assign a value to the node.
 
    NodeItem * author = (NodeItem *)root->pathToNode("root\\sys\\_author");
    if(author != NULL) author->setValue("VoidX-DevTeam");
 
    NodeItem * logo = (NodeItem *)root->pathToNode("root\\sys\\_logo");
    if(logo != NULL) logo->setValue("https://www.voidxdevteam.com/wp-content/uploads/2024/07/logo_square.png");
 
    /* Configure parameter nodes. */
    /* For example, the mix parameter is defined as follows:
       -"mix" as system name (locate the node at root/app/mix). 
       -Assign "Mix" as the display name, visible to the user.
       -Range: 0% to 100%, default: 50%.
       -Linear curve (shape = 0.5f). Log-curve (<0.5), anti-log-curve (>0.5).
       -Inverted = false: parameter increases with value.
       -Displayed as integer (0 decimal values) */
    this->mix = new NodeFloat(parent, "mix", "Mix", 0.0f, 100.0f, 50.0f, "%", 0.5f, false, 0);
    this->feedback = new NodeFloat(parent, "fdbk", "Feedback", 0.0f, 1.0f, 0.5, "", 0.5f, false, 2);
    this->timeNode = new NodeFloat(parent, "time", "Time", 20.0f, 1000.0f, 0, "ms", 0.5f, false, 0);
     
    /* Initialize the graphical engine */
    DrawerHome * drw = new DrawerHome(NULL, NULL, NULL);
    drw->setNodes(parent, System::systemNode(), NULL);
 
    /* Allocate and reset the circular buffer */
    this->delayLine = (float *) memalign(16, DELAY_LINE_SIZE * sizeof(float)); //16 byte memory aligned
    for(int i = 0; i < DELAY_LINE_SIZE; i++) this->delayLine[i] = 0;
    this->delayLineWritePointer = 0;
 
    this->compile();
}

bool DigitalDelay::compile(){}

bool IRAM_ATTR DigitalDelay::exec(DigitalDelay * ptr, float data[SAMPLING_CHANNELS][SAMPLING_FRAME]){}

A robust application foundation has been established with minimal code, incorporating standard user interface management, and VoidX-Control integration.

Let’s code the compile method! To streamline the DSP engine, here we perform some pre-computation, minimizing execution workload. In short, compile translates high-level parameters directly into optimized DSP variables. Compile executes dozens of times per second at a quite high priority.

bool DigitalDelay::compile(){
    
    /* Mix: Convert the percentage value to a 0-1 representation. */
    this->mixDsp = this->mix->getValue() / 100.0f;
 
    /* Feedback: for stability, the actual feedback needs to be strictly less than 1. */
    this->feedbackDsp = 0.999f * this->feedback->getValue();
 
    /* Time: get the amount of delay samples. */
    float msDelay = this->timeNode->getValue();
    this->delaySamplesDsp = (msDelay / 1000.0f) * SAMPLING_FREQ; //SAMPLING_FREQ is 44100 or 48000
    
    //return value
    return false;
}

Let’s tackle the exec method! This is where the low-level DSP processing takes place. exec runs with the highest possible priority.

/* 
The data bi-dimensional array stores the raw audio samples.
-The SAMPLING_CHANNELS constant defines the number of audio channels, usually 2 for stereo sound.
-The SAMPLING_FRAME constant defines the number of audio samples processed together, usually 16 samples per batch.
Input is taken from the Left channel only (Right channel is ignored). Output is equal on both channels.
*/
bool IRAM_ATTR DigitalDelay::exec(DigitalDelay* ptr, float data[SAMPLING_CHANNELS][SAMPLING_FRAME]){
    /* Sample-by-sample unoptimized processing */
    /* Vectorization can significantly improve processing speed. */
    for(int i = 0; i < SAMPLING_FRAME; i++){
        /* Determine the readIndex for retrieving samples from the delay line. */
        int delayLineReadIndex = ptr->delayLineWritePointer - ptr->delaySamplesDsp; 
        if(delayLineReadIndex < 0) delayLineReadIndex += DELAY_LINE_SIZE; //circular buffer
 
        /* Add a new sample to the delay line. */
        /* The new sample is formed by adding the current input to a scaled version of a past output (feedback). */
        ptr->delayLine[ptr->delayLineWritePointer] = data[0][i] + ptr->feedbackDsp * ptr->delayLine[delayLineReadIndex]; 
        ptr->delayLineWritePointer++;
        if(ptr->delayLineWritePointer >= DELAY_LINE_SIZE) ptr->delayLineWritePointer = 0; //circular buffer
 
        /* Mix dry and wet signals, ensuring equal left and right outputs. */
        data[0][i] = data[0][i] + ptr->mixDsp * ptr->delayLine[delayLineReadIndex]; 
        data[1][i] = data[0][i];
    }
    //return value
    //if there is more than one block, returning true will stop the dsp chain execution to this point
    return false;
}

To instantiate the application, add these line at the beginning of main.cpp:

#include "source/DigitalDelay.hpp"

Add the following line inside the app_main method prior to AudioProcessor initialization:

new DigitalDelay(System::appNode(), System::rootNode());

Let’s try it!

The Digital Delay application is now fully functional. You can run it on your Mini Stomp-X platform and tweak the parameters with VoidX-Control! We’ve created a fully-functional modern system in record time.