In Rack parlance, the term “module” refers to the DSP implementation of a virtual Eurorack-style module, while we use the term “plugin” to indicate a collection of modules, all bundled in a folder containing one compiled file, the shared object or dynamic library. Table 4.1 resumes all the terms in the Rack jargon.

Table  4.1:  Some common terms in the Rack jargon

plugin A collection of Eurorack-style modules implemented in a single dynamic library, developed by a single author or company. The Rack browser lists all plugins and allows you to explore all the modules contained in a plugin. Not to be confused with the term as used in the DAW and VST jargon, where it usually refers to one single virtual instrument.
Plugin A C++ struct implementing the plugin. We shall use Plugin in monospaced font only when explicitly referring to the C++ struct.
Model A C++ struct that collects the DSP and the GUI of a Eurorack-style module.
module Not to be confused with the C++ struct Module. We shall use the generic term “module” to indicate either real or virtual Eurorack-style modules.
Module A C++ struct that implements the guts of a module (i.e. the DSP part).
ModuleWidget A C++ struct that implements the graphical aspects of a module.

VCV Rack modules are similar to the “plugins” or “externals” or “opcodes” used in other popular open-source or commercial standards in that they are compiled into a dynamic library that is loaded at runtime by the application (provided that it passes a first sanity check). However, Rack modules differ in all other regards. We shall highlight some of the differences in Section 4.1, then we shall go straight to showing the components required to create a module (Section 4.2), and then give an overview of basic APIs and classes used to define it and make it work (Section 4.3). At the end of this chapter, we shall start the practical work of setting up your coding environment (Section 4.4) and building your first “Hello world” module (Section 4.5).

Please note that some programming basics are assumed along this part of the book. You should have some experience with C++. We will recall some basic properties of the language at different points of the book as a reminder for users with little experience in C++ or developers who are used to working with other programming languages.

4.1 Comparison with Other Platforms

Music programming platforms have existed since the 1950s, starting with the first efforts from Max Mathews at Bell Labs USA with MUSIC I. Mathews had a clear vision of how a comprehensive music computing language needed to be designed. For this reason, by the first half of the 1960s, most of the basic concepts were already implemented in his works, with little variations up to these days. Notwithstanding this, in the decades since, there has been a lot of work to design new music computing platforms, to integrate them with new tools, to make them easier to work with, and to update them to the latest programming standards and languages. A lot of these platforms were born in the academic world and are open-source, usually based on C or C++ (think about Pure Data, Csound, SuperCollider, etc.), while some others are commercial (e.g. Steinberg’s VST and Apple AU). Others were born in an academic context but later taken as a full-fledged commercial project (e.g. Cycling ’74 Max/MSP).

VCV Rack is a commercial project with an open-source codebase, which makes it quite different from the others.

4.1.1 Audio Processing

All the aforementioned music platforms comprise a client application where audio processing objects can be loaded and executed. The client application is made of several parallel tasks, managing audio streams, the user interface, handling network connections – for remote audio (Gabrielli and Squartini, 2016) or for licensing and updating – and more. The musical objects usually do the fun stuff. The term “musical object” is just an umbrella term we need here for generalizing all musical programming platforms. In Pure Data, these objects are called externals, in Csound opcodes, and in VST plugins. In Rack, a plugin is rather a collection of objects, and these objects are often called modules, although the proper name for us programmers is Models.

All musical platforms are based on the concept of dynamic linking of dynamic libraries (i.e. libraries implementing additional functionalities – effects, synthesizers, etc. – that are loaded in the same virtual address space of the client application).

In all the classical music programming platforms, there is an audio engine that handles audio I/O and schedules the execution of the audio processing objects in an order that depends on their connections. The audio engine periodically invokes a special function that each audio processing object implements. This function, which for now we will simply call the audio processing function, is called periodically for signal processing to take place. The audio processing function feeds the object with new audio data and expects processed audio data to be returned. Of course, this is not mandatory. Some objects only take input data (e.g. a VU meter) and do not process it. Other objects have no audio input, but return audio data (e.g. function generators).

The audio engine period is determined by the data rate (i.e. P=B/Fs ), where Fs is the audio sample rate and B is the number of samples to process at each call to the processing function (i.e. the size of the buffer to be processed).

Suppose that we have a sample rate of 48 kHz and we process 64 samples at each iteration. The maximum execution time E64 available is E64<P=6448,000=1.3ˉms, where the “minor than” is to stress the fact that we need to get below the theoretical limit P. In this time frame, the computer needs to not only execute our code, but also all other operating system tasks.

We can also define the real-time factor as the ratio between an execution time and the engine period:

RT=EP%

(4.1)

usually exposed as a percentage. It is obvious that RT must always be less than 100%.

Please note that the audio engine requires some time to pass the output to the sound card, and since we are working with general-purpose computers there is always some random interrupt incoming that has priority over our audio application, so a stable audio processing without glitches can be obtained only with RT factors well below 100% (this is very sensitive on the operating system, the operating system scheduler, the audio driver and many other factors).

For most musical programming applications, the buffer size is a power of 2 and can often be selected by the user to obtain a trade-off between latency and computational resources. Pure Data, for example, by default has a buffer size of 64 samples. To that extent, however, Rack differs from all the other platforms: only one sample at a time is passed to the periodical processing function, called process(...).

The definition of RT factor for Rack changes to:

RT=ET%

(4.2)

because in this case B=1 . The maximum execution time is now E1<148,000=20.8μs, lower than the case above. Fortunately, in this short time, we just have to compute one sample, not 64. If the execution would be linearly proportional to the number of samples to process, then the following would hold: E64=64E1 . In that case, the RT factor would stay the same for batch processing and for sample-wise processing. However, in practice, it turns out that processing one sample at a time will reduce chances to optimize code and exploit instruction pipelining and parallelization. Furthermore, a lot of overhead is added. It thus turns out that E64<64E1 .

If you are wondering why sample-wise processing is less efficient, think about a factory with an assembly line, processing a batch of 100 shoes compared to a single craftsman producing one shoe at a time. The assembly line has specialized workers, each excelling at an operation. Shoes are moved in batch from one machine to another. The assembly line will take less time to make each shoe than the craftsman does, making the factory more efficient (although questions of quality and ethics arise).

At this point, you may be wondering why, in VCV Rack, we are not exploiting the efficiency inherent to process large buffers. In Rack, the main objective is the simulation of electronic circuits, with the ability to create near delayless loops, allowing signal feedback like in modular synthesizers.

Imagine the cascade of a module “A” and a second module “B.” Let the output of “B” be fed back to “A.” In an analog environment, we have a delayless loop. Instantaneous hardware feedback is a key concept for so-called No-Input Mixing music performances. In a discrete-time setup, however, the feedback is delayed by – at least – one sample. Suppose that processing is sample-wise: the computer must, first, execute the instructions for module “A” and extract one output sample. The output is used by module “B” to compute its output sample. At the next time instant, the output of “B” is fed back to “A” and the cycle restarts. If, however, modules A and B are programmed to compute N output values from N input values, the feedback delay increases to N: samples will be computed in batches of N and then fed to the next module. With increasing N, the simulation of a feedback electronic system departs from reality. The only way to get close to a real analog system is thus to process the smallest unit of signal (i.e. a single sample) and make it as short as possible (i.e. increasing the sample rate). The higher the sample rate, the closer to the hardware system, because the time step gets closer to zero.

Sample-wise processing is a necessary ingredient to make Rack get very close to the behavior of modular systems. Of course, this comes with some CPU sacrifice.

4.1.2 Scheduling

Since there is a scheduling order for the audio processing objects, parallel execution of objects in many cases is not an option. Imagine a chain of three objects, implementing an oscillator, a filter, and an amplifier. The audio engine will schedule them serially, as the second needs to wait for the execution of the first, and the third needs the second one to be executed. The audio engine lets each one of them run and return the processed signal, which is subsequently fed to the next object. After the third object has finished, the audio engine can pass the processed signal on to the sound card. This implies that any of the objects may potentially be stealing CPU time to the others. If the available execution time is expired while they have not yet finished processing, there will be audio glitches, as the sound card has no new data to convert and will recycle old data or fill with zeros. The RT factor must be lower than 100% to avoid audio dropouts (i.e. loss of data).

As we said, all music programming platforms have an audio processing function. They have other periodically called functions too. Control data may come in a synchronous or asynchronous way. In the first case, a rate will apply, although lower than the audio engine rate. In the second case, asynchronous events may arise, such as MIDI events or user events. In this context, all platforms exhibit differences. Max, for example, processes control data at a fixed rate of 1 kHz. Rack does not have any control data. This is one of its prominent characteristics: as in modular synthesizers, all information is transmitted by a (virtual) voltage.

Fortunately, current CPUs have multiple cores. Rack is thus able to distribute computing tasks among one or more cores. This means that several process routines are computed at the same time, reducing the risk of dropouts.

Other periodic functions are the GUI rendering functions. In Rack, each graphical widget has a draw method that is called periodically at a rate, ideally, of 60 frames per second.

From this discussion, it should be clear that any musical platform enforces the developer to strip all tasks that are not time-critical or audio-related from the audio processing function, to make it quick. All these accessory tasks should be handled separately in other processes that may be periodically or asynchronously called (Figure 4.1).

Figure  4.1:  Scheduling of the process functions “P” and the draw functions “D” in VCV Rack in a single-core scenario. The process functions are invoked with a period equal to the sample time (i.e. T=1/Fs ), where Fs is the audio sampling rate (e.g. 44,100 Hz). The draw functions are called with a larger period of V=1/Fv , with Fv being the video rate, usually 60 Hz. During the sample time, all the modules must perform their process function. The sum of all their execution times Etot is the total execution time of the patch. The spare time is left to all other lower-priority tasks. The real-time factor RT is thus Etot/T .

4.1.3 Polyphonic Cables

Polyphonic cables are one of the big differences with other modular environments. Although the concept is quite simple, this feature is not available in other common platforms, so their use may not be straightforward at the beginning. Polyphonic cables bundle up to 16 signals into one wire. In essence, all cables in Rack are polyphonic, however, when they revert to monophonic mode if the output port they are connected to is not poly-capable. In such cases, they are shown as thinner. Vice versa, when a polyphonic cable is connected to a mono-only capable input, this may be programmed to discard all channels but the first1 or to sum all of the input components in one.

4.1.4 Expander Modules

Another interesting feature for developers is the possibility to create expander modules. These are modules that do not live on their own, but can be attached to a compatible parent module to expand their functionalities and I/O capabilities. From the developer point of view, expander modules are handled using pointers, allowing any module to be chained with other ones that are on the immediate left and right of it. This feature is not available in platforms such as Pure Data, Max, VST, and so on.

4.1.5 Simplicity and Object-Oriented Programming

One last feature that is unique to Rack is its simple and neat API. This is partly due to the fact that its scope is quite constrained, and a lot of stuff that other platforms have has been wiped away from Rack. Its simplicity is also due to the fact that the API is written from scratch, with the high vantage point of decades of computer music API and with the additional advantage of adopting the latest OOP and C++ standards. Consider the fact that older platforms are starting to wrap their codebase to allow C++ code on top of a bare C core (e.g. this is the case of Csound (Lazzarini, 2017)).

Finally, the ability to write sample-wise processing code makes developing much easier and makes the code easier to read. A lot of aspiring developers started writing code for Rack just by learning from the first few examples provided by VCV (Fundamental, Befaco, and Audible Instruments plugins).

After all, if Rack was not so developer-friendly, this book would never have even been thought of. Credit for all this goes to VCV Rack’s main developer.

4.2 Plugins: Files and Directories

A Rack plugin, practically speaking, requires several components to be found by the application at startup.2 Plugins can be located either in the plugins folder under the installation directory or in the plugins folder under the local Rack directory. The local Rack directory is different for each operating system: it is “My Documents/Rack/” on Windows, “Documents/Rack/” on Mac and “~/.Rack/” on Linux. Each plugin will have a directory with corresponding name under one of the aforementioned plugins folders. The components that are found under each plugin folder are:

  • The compiled object, with different extension depending on the operating system: plugin.so (Linux), plugin.dll (Windows), or plugin.dylib (macOS). This is also known as “shared object” in Linux parlance or “dynamic-link library” under Windows or macOS;
  • The res/folder, usually including SVG graphic files (background) and additional resources (fonts, other graphical elements, etc.);
  • Optional but important: licensing files and a readme.

The plugins are searched by VCV Rack during startup by looking at all folders under Rack/plugins/ and under the local Rack directory. For each folder, the compiled object (plugin.dll or .so or .dylib) file is opened and verified, and if the plugin passes a sanity check it can be loaded.

In the development of plugins, you will also need the source code. Generally, you have a makefile script to compile the source code, under the plugins root (i.e. plugins/<myplugin>/) and the C++ source code under the src/ folder (plugins/<yourplugin>/src/). A typical arrangement of the folder tree is provided in Figure 4.2.

Figure  4.2:  The directory tree of Rack and its plugins.

The compiled object is the core of your project, and results from compiling all your .cpp and .hpp files. The compile process scans the include files for functions, classes, and variables you recall from your code, such as standard C library function (printf, memset, etc.) and specific components such as the functions and classes from Rack. Since the development of the Rack codebase is very quick, the components you include from Rack in your code will vary from version to version. It is thus important that the compiler can find out the correct version of the Rack APIs, the one you mean to use from your code. During compilation, it is also important that the linker can find compiled elements (shared objects and libraries) that match the included files.

Similarly, if you compile a plugin by including and linking files pertaining to a specific version of Rack, you cannot open the same plugin with a different version of Rack. When Rack scans your plugin folder, it will try to load the shared object it finds in it; however, it will give up if there is some mismatch. An example of the warning it will give when a plugin cannot be loaded follows:

 [warning] Failed to load
library ./plugins/ABC/plugin.so: ./plugins/ABC/plugin.so:
undefined symbol: <symbol-name-here>

For this reason, it is crucial to compile and run for the right version of Rack. The “right” version is the one that provides not only the same API, but also the same ABI (application binary interface), that was used for compiling Rack, and thus symbols match.

4.3 Plugins: Software Components

As stated before, a Rack plugin is a collection of models. Each model implements a module and a widget. In a few words, the module describes the inner behavior of the model in relation to voltages and parameters, while the second one describes the graphical appearance of the model and its interaction with the module. Dividing the tasks between module and widget also allows you to create different GUIs for the same module, preview a GUI by creating a model without a module, or run DSP code in a model that has a module but no widget.

In terms of code, Rack provides two main objects:

  • the Module struct; and
  • the ModuleWidget struct.

We will inherit these two objects to define our models. The Module has fundamental members such as the process(...) function, which will be executed at each audio engine cycle, while ModuleWidget instantiates all graphical elements and any other object you may need.

Module is a struct, containing the following important methods and members:

  std::vector<Param> params;
  std::vector<Input> inputs;
  std::vector<Output> outputs;
  std::vector<Light> lights;
  virtual void process(const ProcessArgs &args)   virtual void onSampleRateChange()   void config(int numParams, int numInputs, int numOutputs, int numLights = 0);   void configParam(int paramId, float minValue, float maxValue, float defaultValue, std::string label = ““, std::string unit = ““, float displayBase = 0.f, float displayMultiplier = 1.f, float displayOffset = 0.f)

The first four members are vectors of parameters (e.g. knobs), input and output connectors, and lights that are initialized in the constructor. This means that when we subclass the Module struct, we are able to impose a certain number of these elements. This is hard-coded (we cannot add an output during execution, but this makes absolute sense for a virtual modular system).

The next four methods are important for our scopes. The process method is the fundamental method for signal processing! It is called periodically, once per sample, as discussed in Section 4.1. It takes a constant argument as input: the ProcessArgs structure, that contains global information such as the sampling rate. The config method is necessary to indicate the number of parameters, ports, and lights to create. The configParam describes the range of a parameter, its labels, and more.

Other useful methods that can be overridden are onSampleRateChange, to handle special conditions at the change of the internal engine sample rate, onReset, to handle special conditions during reinitialization of the module, and onRandomize, to make special randomization of the parameters.

The ModuleWidget is the object related to the appearance of the module, and it hosts all GUI elements such as knobs, ports, and so on. It also handles mouse events such as mouse clicks that can be managed by developing custom code:

  Module *module = NULL;
  std::vector<ParamWidget*> params;
  std::vector<PortWidget*> outputs;
  std::vector<PortWidget*> inputs;
  void draw(const DrawArgs &args) override;   void onButton(const event::Button &e) override;   void setPanel(std::shared_ptr<Svg> svg);   void addParam(ParamWidget *param);   void addOutput(PortWidget *output);   void addInput(PortWidget *input);   virtual void appendContextMenu(ui::Menu *menu) {}

The development of novel models is made easy by Rack APIs. Among these, we want to highlight basic components such as:

  • a library of GUI elements, from abstract widgets to knobs and switches (examined in Section 5.2);
  • DSP and signal processing functions and objects, from the Schmitt Trigger (in include/dsp/digital.hpp) to interpolation (in include/math.hpp), from efficient convolution (include/dsp/fir.hpp) to fast Fourier transform (in include/dsp/fft.hpp); and
  • various utilities such as random generator functions, string handling (in include/string.hpp), or clamping and complex multiplications (in include/math.hpp).

We will explore more along these pages; however, you are encouraged to dive into the source code of Rack to inspect all the pre-implemented utilities it features.

4.4 Setting Up Your System for Coding

In this section, we see how to setup a system for building Rack.

The procedure differs for all three operating systems supported, Linux, Windows, and macOS. In all three cases, you will need to work with a terminal (aka console) using Unix-style commands.

For developers who are not used to a Unix terminal, we report a shortlist of bash commands that are useful when working on the terminal to handle your files and folders. The macOS and Linux terminal commands are almost the same. On Windows, you need to install a mingw shell that behaves the same. Some basic commands are:

  • cd <dirname>  change to directory <dirname> (remember that the parent directory is denoted as ../)
  • mkdir <dirname>  creates a new directory <dirname>
  • rm <filename>  removes a file
  • rm -rf <dirname>  removes a directory recursively
  • cp <file> <dest>  copy a file to destination folder or folder/name
  • cp -r <folder> <dest>  copy a folder recursively to a destination path
  • mv <dest>  moves a file or folder src to a destination path or name (you can use it both to rename or to move)

In the following subsections, you will find quick tips to set up your system. However, considering the fast-paced evolution of Rack, things may change slightly in future releases. Check the online Rack manual for changes (https://vcvrack.com/manual/Building.html).

4.4.1 Linux

Several packages need to be installed from your package manager, if not already present.

On Ubuntu, for example, open a terminal window (Ctrl+Alt+T) and type:


  sudo apt install git gdb curl cmake libx11-dev
  libglu1-mesa-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev zlib1g-dev
  libasound2-dev libgtk2.0-dev libjack-jackd2-dev jq

On Arch Linux:

  pacman -S git wget gcc gdb make cmake tar unzip zip curl jq

That’s it.

4.4.2 Windows

You need to install MSYS2, a software package that allows you to manage the building process as in Unix-like platforms. It also features a package manager, very useful to get the right tools easily with one console command. Currently, MSYS2 is hosted at www.msys2.org/, where you can also find the 32- or 64-bit installers. You need to install the 64-bit version. Please also note that Rack is not supported on 32-bit OSs, so it would make no sense anyway to install 32-bit tools.

Once done, launch the 64-bit shell (open the Start menu and look for mingw64 shell) and issue the command:

  pacman -Syu

Then restart the shell and type:

  pacman -Su git wget make tar unzip zip mingw-
  w64-x86_64-gcc mingw-w64-x86_64-gdb mingw-w64-x86_64-cmake autoconf automake mingw-w64-
  x86_64-libtool mingw-w64-x86_64-jq

This will tell the MSYS2 package manager to install the required software tools.

4.4.3 macOS

You need to install Xcode with your Apple ID. Then install Homebrew, a command-line package manager that will help you install the required tools. Go to https://brew.sh/ and follow the instructions to install Homebrew (it is generally done by pasting a command into a terminal window). After Homebrew is ready, in the same terminal you can type:

  brew install git wget cmake autoconf automake libtool jq

This will get you started.

4.4.4 Building VCV Rack from Sources

Once your system is ready you can try building Rack from sources. This procedure is the same for all operating systems. You can decide to compile Rack from the latest sources or get a stable version from the GitHub repository that hosts its code. I suggest you work on an easy-to-reach folder and create a path where you will be able to hold several versions of Rack all compiled, in order to port, backport, or debug your code. On Linux, I use a path such as:

  /home/myusername/Apps/Rack/

and on Windows I use the following path:

C:Rack   (Note: Using MSYS2 shell, you can cd there using: cd /c/Rack)

Inside the Rack folder, I have different versions of Rack (e.g. Rack050, Rack051, Rack-latest, etc.).

Start with:

  git clone https://github.com/VCVRack/Rack.git

This will prepare a local Git repository and download its latest content. If you want instead to go straight with one specific branch (e.g. branch “v1”), you can do the following:

  git clone --single-branch --branch v1 https://github.com/VCVRack/Rack.git

Step in the freshly created repository folder:

  cd Rack

Now you need to download other software modules required by Rack available from Git:

  git submodule update --init --recursive

and other software dependencies that are automatically downloaded and prepared, invoking the following command:

  make dep

This step will take some time and require that you have an Internet connection active in order to download packages. It may happen that the required packages cannot be downloaded for temporary server errors. Take a look at the output. All operations need to be successful, otherwise you will miss fundamental software components and will not be able to compile and run.

The real compilation of Rack is issued by:

  make

And finally, drums rolling:

  make run

will start your compiled version of Rack. Please note that make run will start Rack in development mode, disabling the plugin manager and loading plugins from the local plugins folder.

If you want to speed up the compilation process, you can issue all the make commands adding the “-j <parallel jobs>” option. This starts several parallel jobs, up to the number of parallel threads your CPU supports.

You may encounter some issues in the process. It is very important that you collect the output of the terminal before asking for suggestions on the official forum, on the GitHub issue tracker, or to users of the Rack developers Facebook page.

4.5 Building Your First “Hello World” Module

To build a plugin, you first need to have a working Rack environment compiled from source. The precompiled Rack application obtained from the official VCV web page does not allow developing, so you need to get and prepare a development version alongside it. Go back to Section 4.4 for the details.

The online resources of this book provide a simple “Hello World” module to verify that your system is ready to compile and work with developed modules. Download the HelloWorld plugin folder to the directory of installation of Rack under Rack/plugins/. You will find the source files under src/ and the graphical resources under res/. This plugin contains one module showing a blank screen with the text “Hello World,” following the tradition of coding textbooks.

4.5.1 Build and Run

We first check whether the whole plugin builds fine. Open the terminal, go to the plugin folder, and build using the makefile:

  cd Rack/plugins/HelloWorld
  make

This should be sufficient, and you should see one of the following files in the HelloWorld root, depending on the operating system:

  • plugin.so (Linux)
  • plugin.dll (Windows)
  • plugin.dylib (macOS)

Run Rack to verify that the HelloWorld module is loaded (Figure 4.3):

Figure  4.3:  The Hello World module.

  cd ../../
  make run

Fine, but we can do better than this! However, before starting programming, we should take a look at the source code skeleton we have built so far.

4.5.2 Structure of the Source Files

This section reports the structure of the source files. Keep this section bookmarked whenever you need to create a new plugin or you don’t recall the basic skeleton of a module.

The elements involved with this plugin are:

  • plugin.json. A JSON manifest (i.e. a file containing the information related to the plugin and its modules with a syntax readable to both machines and humans).
  • src/HelloModule.cpp. Implements the module we see and its guts.
  • src/HelloWorld.hpp and src/HelloWorld.cpp. Defines and initializes some pointers and data that allows the plugin (i.e. the module collection) to work.
  • res/CTemplate.svg. The background image file for the module. We shall use this throughout the whole book to give our modules a simple yet elegant look and make them look similar.
  • Makefile. This is the project makefile (i.e. what is executed after the make command is issued). You don’t need to supply this to other users.
  • LICENSE. You should always add a license file to your modules.
  • build/. This folder is created automatically for building. You can disregard its content, and you don’t need to supply this to other users.
  • The plugin dynamic library (plugin.so, plugin.dll, or plugin.dylib, depending on the OS).

The plugin.json file contains data regarding the plugin, including name, authorship and licence, and the modules included in the plugin, with name description and tags. Allowed tags are listed in Rack/src/plugin.cpp (see const std::set<std::string> allowedTags). The plugin.json for the HelloWorld plugin follows:


  {
   “slug”: “HelloWorld”,
   “name”: “Hello World Example”,
   “brand”: “LOGinstruments”,
   “version”: “1.0.0”,
   “license”: “CC0-1.0”,
   “author”: “L.Gabrielli”,
   “authorEmail”: “[email protected]”,
   “authorUrl”: “www.leonardo-gabrielli.info/vcv-book”,
   “sourceUrl”: “www.leonardo-gabrielli.info/vcv-book”,
   “modules”: [
       {
          “slug”: “HelloWorld”,
          “name”: “HellowWorld”,
          “description”: “Empty Module for Demonstration Purpose”,
          “tags”: [“Blank”]
       }
    ]
  }

Note: The plugin version number must follow a precise rule. From Rack 1.0 onward, it is R_MAJOR.P_MAJOR.P_MINOR, where R_MAJOR is the major version of Rack for which you provide compatibility (e.g. 1 for Rack 1.0) and P_MAJOR and P_MINOR correspond to the version of your plugin (e.g. 2.4 if you provided four minor changes to the second major revision of your plugin).

The makefile is a script that instructs the make utility how to build your plugin. You will notice that it includes ../../plugin.mk, which in turn includes ../../arch.mk, a section to spot the architecture of your PC, and ../../compile.mk, the part containing all the compilation flags.

The source code of the plugin includes at least a header and a C++ file, which take the name from the plugin (e.g. HelloWorld.hpp and HelloWorld.cpp). The former gives access to the Rack API by including rack.hpp and declares a pointer to the plugin type. It also declares external models (i.e. the meta-objects) that contain the ModuleWidget (i.e. the GUI) and the Module (i.e. the DSP code) for each one of your virtual modules. The HelloWorld.hpp file looks as follows:

  #include “rack.hpp”
  using namespace rack;
  extern Plugin *pluginInstance;
  extern Model *modelHello;

In HelloWorld.cpp, we instantiate the plugin pointer and we initialize it, adding our modules to it with the addModel() method:


  #include “HelloWorld.hpp”
  Plugin *pluginInstance;
  void init(Plugin *p) {      pluginInstance = p;      p->addModel(modelHello);
  }

Each model is implemented in a separate C++ file. Any such file will include:

  • The Module child class, defining the process(...) method and any other method related to the signal processing.
  • The ModuleWidget child class, defining the GUI and other properties of the module, such as the context menu, and so on.
  • The dynamic allocation of the Model pointer.

The Module is subclassed as follows:

  struct HelloModule: Module {
    enum ParamIds {
       NUM_PARAMS,
   };
    enum InputIds {        NUM_INPUTS,    };
    enum OutputIds {        NUM_OUTPUTS,    };
    enum LightsIds {        NUM_LIGHTS,    };
  HelloModule() {      config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
   }
    void process(const ProcessArgs &args) override {
     ;
   }
  };

As you can see, objects are implemented as a struct. In C++, class and struct are almost equivalent, differing only in the fact that a struct defaults all methods and members to public, while for a class they default to private. For this reason, throughout the book we will sometimes use the term “class” as a synonym of “object,” generically including both “struct” and “class” under this broad term, when both are to be addressed. Since we are following an OOP paradigm, it is worth stressing the difference with a C struct that is intended only as a collection of variables (although compilers nowadays allow struct to have methods even in a C project).

The enums are empty, as you see, and the compiler will default NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, and NUM_LIGHTS to zero. There is no need to use the enums; we could just say:

config(0, 0, 0, 0);

However, you’d better keep this skeleton module more general, to have a little more flexibility in all the practical cases, when you will need to add inputs, outputs, lights, or knobs. Most of the modules we are going to create have non-zero NUM_* enums.

As you can see, the class also declares the process(…) method, stating that it will override the same method from the base class Module. The implementation is empty for now as the HelloModule class does not define any action on signals; it is just a blank panel (more on blank panels in Section 10.8.3).

We then define the module widget child class and implement its constructor:

  /* MODULE WIDGET */
  struct HelloModuleWidget: ModuleWidget {
    HelloModuleWidget(HelloModule* module) {
        setModule(module);         setPanel(APP->window->loadSvg(asset::plugin   (pluginInstance, “res/HelloModule.svg”)));
     }
  };

As you can see, HelloModuleWidget inherits the ModuleWidget class. The constructor needs to have a pointer to the Module subclass it will handle. More on this later. The setPanel method takes an SVG file, loaded with loadSvg, and applies it as background to the module panel. Please remember that, by default, the module size will be adapted to the size of the SVG file. The SVG file must thus respect the size specifications, which impose the height of the module to be 128.5 mm (3 HU in the Eurorack format) and the width to be an integer multiple of 5.08 mm (1 HP). We will discuss the creation of the SVG panel in more detail in Chapter 5. Please note that the panel size can be changed from code, as we shall see for the ABC plugins.

When the zoom is 100%, a module height of 128.5 mm corresponds to 380 pixels:

#define RACK_GRID_HEIGHT 380 // include/app/common.hpp

Finally, the file ends with the creation of the Model, indicating the Module and the ModuleWidget as template arguments:

Model * modelHello = createModel<HelloModule, HelloModuleWidget>(“HelloWorld”);

The input argument takes the slug of the module. The template arguments are the subclass of Module and ModuleWidget.

Now Rack has all the required bits to compile your Model and add it to the plugin.

4.6 Adopting an IDE for Development: Eclipse

Now you have the basic tools for compiling a project. However, writing and managing code can be complex sometimes. You will need a text editor with syntax highlighting at least. Each operating system has its own good editors. If you additionally want to manage your source code, watch your project structure, launch a build, and have automatic suggestions and code verification, then an integrated development environment (IDE) is better suited.

One option I would suggest to all readers is the Eclipse platform,3 maintained by the Eclipse foundation, open-source and highly flexible. This tool is cross-platform (Linux, Windows, macOS) and is available in different flavors, depending on needs and the language you use. For C/C++ developing, I recommend downloading a build of the C/C++ developers IDE, or adding the Eclipse CDT package to an existing Eclipse installation.

Eclipse (and similarly most C/C++ IDEs) shows up with several distinct panels, for efficient project management. The Eclipse CDT shows a main panel with multiple tabs for reading and writing the source code, a Project Explorer for browsing projects and the related files, and additional panels for searching keywords, building, debugging, and so on. A typical Eclipse window is shown in Figure 4.4.

Figure  4.4:  The Eclipse IDE, with the Project Explorer (left panel) for browsing projects and files, the main panel (top right) showing multiple source files, and the Console panel (bottom right) showing the makefile log after building the ABC project.

4.6.1 Importing a “Makefile Project”

Any existing project with a makefile can be imported, and to exploit automatic C/C++ code indexing the Rack source can be referenced. We shall describe these few steps here, leaving the reader to the online Eclipse documentation or other reference texts for instructions to use Eclipse.

We will first import the Rack codebase, needed by the Eclipse C/C++ indexing tool. The indexing tool is what makes automatic suggestion and automatic error/warning detection possible. Open Eclipse and go to File → New → Makefile Project with Existing Code. Select a location and give the project a name. The location should be the root of your Rack installation (i.e. where the makefile resides). I suggest including the version in the project name (e.g. Rack101 for v1.0.1), so you will be able to add future versions to Rack without having to delete previous ones. The toolchain is not important – you can leave it to <None>.

Now, similarly, we will import the ABC library (or any other plugin folder) with File → New → Makefile Project with Existing Code. The project location should be the ABC folder, where the makefile resides, and I suggest you include the version of Rack you are compiling against in the project name.

Now you have the ABC project in the project explorer. Right-click it and go to Properties. Select Project References and check the Rack project name. Now the indexer will include Rack in its search for classes and declarations. To force building the C/C++ index, go to Project → C/C++ Index → Rebuild.

Now open a source file (e.g. AComparator). If you Ctrl+Click a Rack object name (e.g. Module), Eclipse will take you to its declaration. This speeds up your learning curve and development time!

You can also experience automatic suggestions. Automatic suggestions are available by pressing Ctrl+Space. The Eclipse indexing tool will scan for available suggestions. By typing in more letters, you will refine the research by limiting it to words starting with the typed letters. If you write, for example, the letter “A” and then press Ctrl+Space, you will get a large number of suggested objects, functions, and so on. However, if you type “AEx,” you will get only “AExpADSR,” a class that we have implemented in our ABC project (Figure 4.5).

Figure  4.5:  Eclipse code editor showing automatic suggestions, available by pressing Ctrl+Space.

4.6.2 Building the Project Inside Eclipse

Any C/C++ project in Eclipse can be built from the IDE itself. If we import the project as a “Makefile Project,” the IDE will know that it just has to issue the make command as we would do ourselves into a terminal. You can check this by right-clicking on the name of the project in the Project Explorer and clicking Properties, or going to Project → Properties. On the left, you will see C/C++ Build, and the Builder Settings should be set by default so that it uses the default build command (“make”). The build directory is the root folder of your plugin. This is exemplified in Figure 4.6.

Figure  4.6:  The Project settings window, showing default settings to be used for building the ABC project. As you can see, the build command is the default one (make).

To test whether the Build command works fine, go to Project → Build Project. You should see the log of the build process in the Console window, as seen in Figure 4.4. You should see no errors in the log.

This section was a small hint to setting up your Eclipse IDE and getting ready to go. We will not go further into the vast topic of developing with Eclipse, as it would be out of our scope, but you are encouraged to read the documentation, or compare with any other IDE you are already familiar with.

Notes

1    This is the suggested behavior that should be followed by all developers.

2    Rack loads the plugins only once at startup, so you need to restart Rack if any of the plugins have been updated.

3    Experienced developers will have their own choice for the IDE, and some old-school developers will prefer developing from a terminal using applications such as Vi or Emacs. Some will complain that I’m suggesting this IDE instead of another one. That’s fine – let’s make peace, not war. Eclipse will suit all platforms and integrates nicely with VCV Rack – that’s why I’m suggesting it in the first place.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.23.30