The boring part is over; from here on. we are going to build and test modules!

If you followed the previous chapters, you are already able to compile a module and edit its graphical user interface. In this chapter, we start with coding. The easiest way to take acquaintance with coding with VCV Rack is to build “utility” modules that are functional but do not require extensive DSP knowledge or complex APIs. We are going to set up a comparator module, a multiplexer/demultiplexer (in short, mux and demux), and a binary frequency divider, shown in Figure 6.1.

Figure  6.1:  The modules presented in this chapter.

6.1 Creating a New Plugin from Scratch, Using the Helper Script

Before we start with the modules, we need to create the basics for a new plugin. This will allow us to add modules one at a time.

The basic steps follow:

  • Create a new folder in Rack/plugins/.
  • Create the folders for the source code and the resources, src/ and res/.
  • Generate a plugin.json manifest.
  • Add a license file and a readme (not strictly necessary, but useful for sharing with others).
  • Add a Makefile.
  • Add the C++ and header files for the plugin.

To get this work sorted out quickly and free of errors, the Rack helper script under Rack/helper.py again comes in handy.

Go to Rack/plugins/ and invoke it:

cd Rack/plugins/

../helper.py createplugin myPlugin

An interactive prompt will ask for all the details that will go into the JSON manifest. A folder including all the necessary files will be created, leaving you ready for the task.

During the rest of the book, we shall work on the development of the ABC plugin collection, thus the plugin name will be ABC, and all the modules will start with the letter “A” (e.g. AComparator, etc.).

6.2 Comparator Module

TIP: In this section, we will discuss the whole code and process involved in creating your first complete module.

As a first example, we start with a simple module, a comparator. Given two input voltages, a comparator module simply provides a high output voltage if input 1 is higher than input 2, or a low voltage otherwise. In our case, the high and low voltages are going to be 10 V and 0 V. The module we are going to design has:

  • two inputs;
  • one output; and
  • one light indicator.

These are handled by the process(…) function, which reads the input and evaluates the output value. The light indicator also follows the output. We shall, first, describe the way inputs, outputs, and lights are handled in code.

Let us first concentrate on monophonic inputs and outputs. As discussed in Section 4.1.3, Rack provides a fancy extension to real-world cables (i.e. polyphonic cables), but for the sake of simplicity we shall leave these for later.

Input and output ports are, by default, monophonic. Three methods allow you to access basic functionalities valid for all type of ports:

  • isConnected();
  • getVoltage(); and
  • setVoltage(float voltage).

The first method allows you to check whether a cable is connected to the port. The getVoltage() method returns the voltage applied to the port. This is normally used for reading the value of an input port. Finally, the last method sets the voltage of the port (i.e. assigns a value to an output port). These three methods are enough for most of the functionalities we shall cover in this book.

The lights have their own set/get methods:

  • getBrightness(); and
  • setBrightness(float brightness).

These clearly have similar functionalities; however, we are going to use the latter most of the time to assign a brightness value to the lights.

While the port voltage follows the Eurorack standards (see Section 1.2.3), the lights get a brightness value in the range [0, 1]. The value is squared, so negative values will be squared too, and larger values are clipped.

Now that you have all the necessary bits of knowledge, we can move to the development of the comparator module. The development steps (not necessarily in chronological order) follow:

  • Prepare all the graphics in res/.
  • Add the module to plugin.json.
  • Add the C++ file AComparator.cpp to src/.
  • Develop a struct (Acomparator) that subclasses Module.
  • Develop a struct (AComparatorWidget) that subclasses ModuleWidget and its constructor.
  • Create a model pointer modelAComparator that takes the above two structs as template arguments.
  • Add the model pointer to the plugin into ABC.cpp.
  • Declare the model pointer as extern in ABC.hpp.

Naturally, the helper script may speed up the process.

We will start defining how the module behaves. This is done by creating a new struct that inherits the Module class.

Let’s take a look at the class definition, which shall be placed in AComparator.cpp:


struct AComparator : Module {
enum ParamIds {
NUM_PARAMS,
};
enum InputIds {
    INPUTA1,
    INPUTB1,
    INPUTA2,
    INPUTB2,
    NUM_INPUTS,
};
enum OutputIds {
    OUTPUT1,
    OUTPUT2,
    NUM_OUTPUTS,
};
enum LightsIds {
    LIGHT_1,
    LIGHT_2,
    NUM_LIGHTS,
};
AComparator() {
   config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
}
void process(const ProcessArgs &args) override;
};

The module defines some enums that conveniently provide numbering for all our inputs, outputs, and lights. You may want to give meaningful names for the enums. Always remember that, generally, enums start with 0.

We don’t have members for this module, so we go straight into declaring the methods: we have a constructor, AComparator(), and a process(…), which will implement the DSP stuff. That’s all we need for this basic example.

The constructor is the place where we usually allocate stuff, initialize variables, and so on. We leave it blank for this module and go straight into the process function.

The process(…) function is the heart of your module. It is called periodically at sampling rate, and it handles all the inputs, outputs, params, and lights. It implements all the fancy stuff that you need to do for each input sample.

Each time the process function is executed, we compare the two inputs and send a high-voltage or a low-voltage value to the output. This is done by a simple comparison operator.

In C++ code:

void AComparator::process(const ProcessArgs &args) {
    if (inputs[INPUTA1].isConnected() && inputs[INPUTB1]. isConnected()) {     float out = inputs[INPUTA1].getVoltage() ˃= inputs[INPUTB1]. getVoltage();         outputs[OUTPUT1].setVoltage(10.f * out);         lights[OUTPUT1].setBrightness(out);     } }

We want to spare computational resources and avoid inconsistent states, thus sometimes it is better to avoid processing the inputs if they are not “active” (i.e. connected to any wire). If they are both connected, we evaluate their values and use the operator “?” to compare the two. The result is a float value that takes the values of 0.f or 1.f. The output goes to the light (so we have visual feedback even without an output wire) and to the output voltage.

Now that the module guts are ready, let’s get started with the widget. We declare it in AComparator.cpp. This widget is pretty easy and it only requires a constructor to be explicitly defined.

struct AComparatorWidget : ModuleWidget {
    AComparatorWidget(AComparator* module) {
      setModule(module);       setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, “res/AComparator.svg")));
      addInput(createInput<PJ301MPort>(Vec(50, 78), module,         AComparator::INPUTA1));
      addInput(createInput<PJ301MPort>(Vec(50, 108), module,         AComparator::INPUTB1));           addOutput(createOutput<PJ3410Port>(Vec(46, 138), module,         AComparator::OUTPUT1));
      addChild(createLight<TinyLight<GreenLight˃˃(Vec(80, 150),         module, AComparator::LIGHT_1));     } };

What is the constructor doing? It first tells AComparatorWidget to take care of AComparator by passing the pointer to the setModule(…) function. It then sets the SVG graphic file as the panel background. The file is loaded as an asset of the plugin and automatically tells Rack about the panel width.

At the end of the widget, we add other children as the inputs and outputs. Let’s have a detailed look into this. The addInput method requires a PortWidget*, created using the createInput method. This latter method requires a port template, which basically defines the graphical aspects (as far as we are concerned now) of this new port. The creation argument of the port defines the position, in terms of a (x,y) coordinate vector Vec, the parent module, and the port number of that module. We use, for convenience and readability, the enum name instead of a number. Similarly, we do for the output ports and the parameters (although we do not have them in this example). Other widgets, such as lights and screws,1 use an addChild method.

The widget configuration, as you can see, is pretty basic, and most modules will follow the same guidelines:

Model * modelAComparator = createModel<AComparator,
AComparatorWidget>("AComparator");

Finally, the Model is created, setting the Module subclass and the ModuleWidget subclass as template arguments and the module slug as input argument.

The last step is to create this model and add it to the list of models contained in our plugin. We do this in ABC.cpp inside the init(rack::Plugin *p) function as:

p->addModel(modelAComparator);

and we declare this model pointer as extern in ABC.hpp, so that the compiler knows that it exists:

extern Model * modelAComparator;

OK, pretty easy! We can compile the code against the provided version of the VCV Rack source code, et voila, the module is ready to be opened in VCV Rack!

Now it is time for you to test your skills with some exercises. You will find this simple code in the book repository. Go to the exercise section of this chapter to see how to expand the module and improve your skills!

Exercise 1

The comparator module has only two inputs and an output port. Wasting space is a pity, so why don’t we replicate another comparator so that each module has two of them? You can now try to add two extra inputs and one extra output and light. You can double most parts of the code, but I suggest you iterate with a for loop through all the inputs and outputs. Try on your own, and at the end – if you can’t figure it out on your own – take a look at how this is done in the AComparator found with the online resources.

Exercise 2

Sometimes you want the output to latch for a few milliseconds (i.e. to avoid changing too quickly). Think of the case where input “A” is a signal with some background noise and input “B” is zero. All the times that “A” goes over zero, the output will get high. This will happen randomly, following the randomness of the noisy input “A” signal. How do you avoid this? A hysteresis system is what you need. Try to design yourself a conditional mechanism that:

  • drives the output high only when input “A” surpasses a threshold that is slightly higher than input “B”; and
  • drives the output low only when input “A” drops below a threshold that is slightly lower than input “B.”

6.3 Muxing and Demuxing

  TIP: In this section, you will learn how to add parameters such as knobs, and to pick the right one from the component library.

The terms “mux” and “demux” in engineering stand for multiplexing and demultiplexing, which are the processes that make multiple signals share one single medium, in our case a wire (or its virtual counterpart). In analog or virtual circuits, we need multiplexers to allow two or more signals to be transferred over a wire, one at a time. Similarly, we demultiplex a wire when we redirect it to multiple outputs. This is achieved in analog circuits by means, for example, of voltage-controlled switches. In the digital domain, we do it a bit differently.

Considering an integer variable, selector, holding the value of the input port to be muxed to the output, a C++ snippet for a multiplexer could be as follows:

output = inputs[selector]; // MUX

Similarly, a C++ snippet for a demultiplex, where one input can be sent to one of many outputs, looks like:

outputs[selector] = input; // DEMUX

How do we translate this into Rack port APIs? Try this on your own first – write it on paper, as an exercise.

This second module we are designing requires a selector knob. This is a parameter, in the Rack terminology. Let us discuss how parameters work and how to add them to a module. Parameters may be knobs, sliders, switches, or any other graphical widget of your invention that can store a value and be manipulated by the user. The two important methods to know are:

  • getValue(); and
  • setValue(float value).

As with lights and ports, the set/get methods return or set the value of the parameter. While the latter is of use in some special cases (e.g. when you reset or randomize the parameter), you will use the getValue() most of the time to read any change in the value from the user input.

The parameters are added to the ModuleWidget subclass to indicate their position and bind them to one of the parameters in the enum ParamIds. Let us look at this example:

addParam(createParam<ParameterType>(Vec(X, Y), module, 
MyModule::OUTPUT_GAIN));

The details you have to input in this line are the ParameterType (i.e. the name of a class defining the graphical aspect and other properties of the object, the positioning coordinates X and Y, and the parameter from the enum ParamIds of the Module struct to which you bind this parameter).

The last bit of information to give to the system is the value mapping and a couple of strings. All these are set through the configParam method, which goes into the Module constructor. One example follows:

configParam(OUTPUT_GAIN, 0.f, 2.f, 1.f, “Volume”);

In this case, we are telling the Module that the knob or slider related to the parameter OUTPUT_GAIN should go from 0 to 2, and by default (initialization) will be 1. The string to associate to it when right-clicking is “Volume.” Further arguments allow you to make this more powerful, but we’ll see this later in this chapter.

Now let us move through the implementation of the whole module, AMuxDemux. The module will host both mux and demux sections, and thus two selector knobs will be necessary. We shall have four inputs for the mux and four outputs for the demux. We shall also place light indicators near each of the selectable inputs for the mux, and near each of the selectable outputs for the demux.

In our case, if we define the enums as:

enum ParamIds {
    M_SELECTOR_PARAM,
    D_SELECTOR_PARAM,
    NUM_PARAMS,
};
enum InputIds {
    M_INPUT_1,
    M_INPUT_2,
    M_INPUT_3,
    M_INPUT_4,
    D_MAIN_IN,
    NUM_INPUTS,
    N_MUX_IN = M_INPUT_4,
};
enum OutputIds {
    D_OUTPUT_1,
    D_OUTPUT_2,
    D_OUTPUT_3,
    D_OUTPUT_4,
    M_MAIN_OUT,
    NUM_OUTPUTS,
    N_DEMUX_OUT = D_OUTPUT_4,
};
enum LightsIds {     M_LIGHT_1,     M_LIGHT_2,     M_LIGHT_3,     M_LIGHT_4,     D_LIGHT_1,     D_LIGHT_2,     D_LIGHT_3,     D_LIGHT_4,     NUM_LIGHTS, };

we declare two integer variables to hold the two selector values. To make their values consistent across each step of the process(…) method, we declare them as members of the AMuxDemux struct:

unsigned int selMux, selDemux;

They are zeroed in the Module constructor, where the value mapping of the selectors is also defined:

AMuxDemux() {
     config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
     configParam(M_SELECTOR_PARAM, 0.0, 3.0, 0.0, “Mux Selector");
     configParam(D_SELECTOR_PARAM, 0.0, 3.0, 0.0, “Demux Selector");
     selMux = selDemux = 0;
}

We can now implement the process method as follows:

void AMuxDemux::process(const ProcessArgs &args) {
/* MUX */ lights[selMux].setBrightness(0.f); selMux = (unsigned int)clamp((int)params[M_SELECTOR_PARAM].getValue(), 0, N_MUX_IN); lights[selMux].setBrightness(1.f);
if (outputs[M_MAIN_OUT].isConnected()) {       if (inputs[selMux].isConnected()) {
outputs[M_MAIN_OUT].setVoltage(inputs[selMux].getVoltage());       } } /* DEMUX */ lights[selDemux+N_MUX_IN+1].setBrightness(0.f); selDemux = (unsigned int)clamp((int)params[D_SELECTOR_PARAM].getValue(), 0, N_DEMUX_OUT); lights[selDemux+N_MUX_IN+1].setBrightness(1.f);
if (inputs[D_MAIN_IN].isConnected()) {       if (outputs[selDemux].isConnected()) {
outputs[selDemux].setVoltage(inputs[D_MAIN_IN].getVoltage());       }  } }

As you can see, there are several checks and casts. We need to cast the input value to an integer because it is used as an index in the array of inputs. The selector values are always clamped using Rack function clamp(), which clamps a value between a minimum and a maximum. This is a little paranoid, but it is very important that the selector value does not exceed the range of the arrays they index, otherwise an unpleasant segmentation fault is issued and Rack crashes. You can avoid this if you make the parameters D_SELECTOR_PARAM and M_SELECTOR_PARAM be constrained properly in your widget.

You will notice that we shut off a light each time before evaluating the new value for selDemux or selMux, even if the selector has not changed. This makes the code more elegant and reduces the number of if statements.

Finally, the lines concerning the outputs are reached only if the relevant inputs and outputs are connected. The assignments to the output ports are done using the setVoltage() and getVoltage() methods. Compare this with the lines you wrote previously for the exercise. Did you get the thing right?

To conclude the module, let us look at the graphical aspect. We defined through the configParam method that the selectors span the range 0–3. The last step is to tell the ModuleWidget subclass where to place the selector, and what it looks like. The latter is defined by the template <ParameterType> of the createParam method, seen above. We have a lot of options – just open the include/componentlibrary.hpp and scroll. This header file defines a lot of components: lights, ports, and parameters. Let us look at the knobs: RoundKnob, RoundLargeBlackKnob, Davies1900hWhiteKnob, and so on – there are a lot of options! As discussed in Section 5.2, they are organized hierarchically, by inheritance, and their properties are easy to interpret from the source code directly.

Besides the graphical aspect, there is one important property to make the mux/demux module work flawlessly: the snap property. Knobs can snap to integer values, as a hardware selector/encoder would do, skipping all the real-valued position between two integer values. This way, the user can select which of the input/output ports to mux/demux easily, with a visual feedback. We take the RoundBlackSnapKnob from the component library and place it where it fits as follows:

 addParam(createParam<RoundBlackSnapKnob>(Vec(50, 60), module,
 AMuxDemux::M_SELECTOR_PARAM));

Before moving on to the last remarks, take a look at the provided code, AMuxDemux.cpp. The module implements both a multiplexer (upper part) and a demultiplexer (bottom part), so you can test it easily.

Note: The function clamp is an overloaded function. This means that there are two definition of the function, one for integer and one for float variables. You can use the same function name with either float or integer values, and the compiler will take care of figuring out which one of the two implementations to use, depending on the input variable type.

Exercise 1

What about employing an input to select the output (input) of a multiplexer (demultiplexer)? This is handy as we may want to automate our patches with control voltages. To implement this, it is sufficient to replace the param variable with an input variable.

Exercise 2

You may decide to have both a knob and an input as a selector. How could you make the two work together? There is no standard for this, and we may follow different rules:

  • Sum the input CV and the knob values after casting to int: this is quite easy to understand for the user. It may not be of use if the knob is turned all the way up: the input signal will not affect the behavior in any way unless it is negative.
  • Sum the input and the knob values and apply a modulo operator: this way, there will always be a change, although it may be more difficult to have control on it.
  • Exclude the knob when the CV input is active.

6.4 Knobs: Displaying, Mapping, and Computing Their Values

Before going on with the development of modules, we should focus on knobs, or more generally on parameters. The configParam was introduced in Section 4.3; however, now it is time to discuss it in more detail. The arguments it takes are:

  1. paramId. The parameter we are configuring.
  2. minValue. The minimum value.
  3. maxValue. The maximum value.
  4. defaultValue. The default value for initialization or reset.
  5. label. The name that is visualized.
  6. unit. The measurement unit (if any) to be displayed.
  7. displayBase. Used to compute the displayed value.
  8. displayMultiplier. Multiplies the displayed value.
  9. displayOffset. Adds an offset to the displayed value.

The last three arguments can be important to improve the user experience: they are meant to compute a value that is displayed to the user in order for the knob to make sense. This value is displayed in the parameter tooltip or when right-clicking on the parameter. Depending on displayBase, the way the displayed value is computed changes:

{vm+ob=0log(ν)log(b)m+ob<0bνm+ob>0

(6.1a)
(6.1b)
(6.1c)

where b, m, and o are displayBase, displayMultiplier, and displayOffset, respectively, and v is the value given by the knob, in the range minValue to maxValue.

When the last three arguments are discarded, the parameters scroll linearly from the minValue to maxValue. When the base b is zero but any of m and/or o are provided, the value is computed according to Equation 6.1a (i.e. a multiplication factor and an offset can be applied). If the base is negative, the logarithmic case applies, as in Equation 6.1b. The natural logarithm of –b (to make it positive) is used to scale the natural logarithm of the knob value. A multiplier and an offset are still applied after computing the logarithms. Please note that the denominator should not be zero, thus b 1 . Finally, if the base is positive, the base is raised to the power of v.

Examples of these mappings are shown in Figure 6.2.

Figure  6.2:  Mapping the knob value into a displayed value according to the configParam arguments. In (a), the values v, ranging from 0.1 to 10, are shown. In (b), the mapping of Equation 6.1a is shown with m=2,o=20 . In (c) and (d), the mappings are shown according to Equation 6.1b for b=0.1 and b=10 , respectively. In (e) and (f), the exponentials are shown with b=0.1 and b=2 .

Please note that the displayed value is computed by the GUI in Rack only for display. The value provided by the Param::getValue method will always be v (i.e. the linear value in the range from minValue to maxValue). This leaves you free to use it as it is, to convert it in the same way it is computed according to the configParam, or even compute in a different way. Considering the computational cost of transcendental functions such as std::pow or std::log, you should avoid evaluating the value according to Equation. 6.1b and Equation 6.1c at each time step. Good approximations of logarithms can be obtained by roots. Roots and integer powers of floats are quicker to compute than logarithms or float powers.

In the following chapters, different strategies will be adopted to map the parameters according to perception and usability.

6.5 Clock Generator

TIP: In this section, you will learn how to generate short clock pulses using the PulseGenerator Rack object.

A clock generator is at the heart of any modular patch that conveys a musical structure. The notion of a clock generator differs slightly among the domains of music theory, computer science, communication, and electronics engineering. It is best, in this book, to abstract the concept of clock and consider it as a constant-rate events generator. In the Eurorack context, an event is a trigger pulse of short duration, with a voltage rising from low to high voltage. In VCV Rack, the high voltage is 10 V and the pulse duration is 1 ms.

In this section, we will develop a simple clock that generates trigger pulses at a user-defined rate expressed in beats per minute (bpm). The generated pulses can be used to feed a frequency divider or an eight-step sequencer as those developed in this chapter.

The operating principle of a digital clock is simple: it takes a reference clock at frequency fref and scales it down by issuing an event every L pulses of the reference clock. The obtained frequency is thus fclk=frefL . The reference clock must be very stable and reliable, otherwise the obtained clock will suffer jitter (i.e. bad timing of its pulses). In the hardware domain, a reference clock is a high-frequency PLL or quartz crystal oscillator. In the software domain, a reference clock is a reliable system timer provided by the operating system or by a peripheral device. The clock generator will count the number of pulses occurring in the reference clock and will issue a pulse itself when L pulses are reached.

As an example, consider a reference clock running at 10 kHz. If I need my clock generator to run at 5 kHz, I will make it issue a pulse every second pulse of the reference clock. Each time I pulse my clock, I also need to reset the counter.

In Figure 6.3, the frequency to be generated was an integer divisor of the reference clock frequency. However, when we have a clock ratio that is not integer, things change.

Figure  6.3:  Generation of a 5 kHz clock from a 10 kHz clock by firing a pulse each second pulse of the reference clock.

Let us consider the following case. We need to generate a 4 kHz clock from a 10 kHz clock. The exact number of pulses to wait from the reference clock before firing a pulse is L4k=10kHz4kHz=2.5, which is not integer. With the aforementioned method, I can only count up to 2 or 3 before issuing a pulse. If I fire a pulse when the counter reaches 3, I generate a clock that is 3.3 kHz, yielding an error of 0.6 kHz, approximately 666 Hz. Not very precise. Firing a pulse when the counter reaches 2 means generating a clock at 5 kHz, which is even farther from 4 kHz.

If I could increase the frequency of the reference clock, the error would be lower, even zero. If I have a clock at, for example, 100 kHz, I could count up to 25 and obtain a clock of 4 kHz. A larger clock ratio fref/fclk is thus of benefit to reduce the error or even cancel it, where fref is a multiple of fclk .

However, if I cannot change the ratio between the two clocks, a better strategy needs to be devised. In software, we can use fractional numbers for the counter. One simple improvement can thus be done by using a floating-point counter instead of an integer one. Let us again tackle the case where we want to generate a 4 kHz clock from a 10 kHz reference clock. As before, I can still fire a pulse only in the presence of a pulse from the reference clock, which is equivalent to setting an integer threshold for the counter, but this time I allow my counter to go below zero and have a fractional part.

The counter threshold Lˆ is set to 2 and the floating-point counter starts from zero. At the beginning, after counting two pulses of the reference clock, a pulse will be fired, but without resetting the counter to 0. Instead, L4k is subtracted from the counter, making it go to −0.5. After two oscillations of the reference clock, the counter will reach 1.5, not yet ready to toggle. It will be toggled after the third reference pulse because the counter goes beyond 2 (more precisely, it reaches 2.5). At this point, L4k is subtracted and the counter goes to 0, and we get back to the starting point. If you continue with this process, you will see that it fires once after two reference pulses and once after three reference pulses. In other words, the first pulse comes too early, while the second comes too late. Alternating the two, they compensate each other. If you look on a large time frame (e.g. over 100 reference clock pulses), you see 40 clock pulses (i.e. the average frequency of the clock is 4 kHz and the error – on average – is null). If you think about the whole process, it is very similar to the leap year (or bissextile year), where one day is added every four years because we cannot have a year that lasts for 365 days and a fraction.2

Please note that this way of generating a clock may not be desirable for all use cases, because the generated clock frequency is correct only on average. In fact, it slightly deviates from period to period, generating a jitter (i.e. an error between the expected time of the pulse and the time the pulse happens). In the previous example, the jitter was heavy (see Figure 6.4) because we had a very low clock ratio. In our scopes, however, the method is more than fine, and we will not dive into more complex methods to generate a clock. We are assuming that the clock ratio will always be large because we will be using the audio engine as a clock source to generate bpm pulses, which are times longer than the sample rate. The audio engine is very stable and reliable, otherwise the audio itself would be messy or highly jittered. The only case when the assumption fails is when the system is suffering from heavy load, which can cause overruns or underruns. However, in that case, the audio is so corrupted that we won’t care about the clock generator module. The audio engine also provides a large clock ratio if we intend to build a bpm clock generator (i.e. a module that at its best will generate a few pulses per second). Indeed, the ratio between the reference clock (44,100 at lowest) and the bpm clock is much larger than the example above. While in the example above the reference clock was less than an order of magnitude faster, in our worst case here we have a reference clock that is more than three orders of magnitude faster. If you take, for example, 360 bpm as the fastest clock you want to generate, this reduces to 6 beats per second, while the audio engine generates at lowest 44,100 events per second. The ratio is 7,350. In such a setting, the jitter cannot be high, and the average error, as shown above, reduces to zero.

Figure  6.4:  The process of generating a clock that has an average frequency of 4 kHz from a 10 kHz clock source. The figure shows the reference clock (a), the floating-point counter (b), the clock triggered by the counter (c), which will be 4 kHz only on average, the desired clock at precisely 4 kHz (d), and the time difference between the generated clock and the desired one (e). Although the generated clock has an average frequency of 4 kHz, it is subject to heavy jitter because of the low clock ratio.

Let us now see how to build the module in C++.

We first describe the PulseGenerator object. This is a struct provided by Rack to help you create standard trigger signals for use with any other module. The struct helps you generate a short positive voltage pulse of duration defined at the time of triggering it. You just need to trigger when needed, providing the duration of the pulse, and then let it go: the object will take care of toggling back to 0 V when the given pulse duration is expired. The struct has two methods:

  • process(float deltaTime); and
  • trigger(float duration).

The latter is used to start a trigger pulse. It takes a float argument, telling the duration of the pulse in seconds (e.g. 1e-3 for 1 ms). The process method must be invoked periodically, providing, as input argument, the time that has passed since the last call. If the pulse is still high, the output will be 1. If the last pulse trigger has expired, the output will be 0. Always remember that you need to multiply the output of the process function to obtain a Eurorack-compatible trigger (e.g. if you want to generate a 10 V trigger, you need to multiply by 10.f).

Figure 6.5 shows a trigger pulse generated by PulseGenerator, where the trigger method is first called telling the impulse duration. At each process step, the output is generated, checking whether the trigger duration time has passed, and thus yielding the related output value.

Figure  6.5:  Schematic functioning of the PulseGenerator object.

For modules generating a clock signal, a good duration value is 1 ms (i.e. 0.001 s, or 1e-3f in the engineering notation supported by C++). A positive voltage of 10 V is OK for most uses.

We now move to the module implementation, discussing how to convert bpm to a period expressed in Hz, and how the floating-point counter algorithm described above is put into practice.

The clock module is based on the struct AClock, a Module subclass, shown below:

struct AClock : Module {
  enum ParamIds {
    BPM_KNOB,
    NUM_PARAMS,
  };
  enum InputIds {
    NUM_INPUTS,
  };
  enum OutputIds {
    PULSE_OUT,
    NUM_OUTPUTS,
  };
  enum LightsIds {
    PULSE_LIGHT,
    NUM_LIGHTS,
  };
  dsp::PulseGenerator pgen;   float counter, period;
  AClock() {     config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);     configParam(BPM_KNOB, 30.0, 360.0, 120.0, “Tempo", “BPM");     counter = period = 0.f;   }
  void process(const ProcessArgs &args) override;  };

The enums define a knob for bpm selection, one output, and a light to show when the trigger is sent. The knob is configured with minimum, maximum, default values, and with two strings: the name of the parameter and the unit, in this case beats per minute (bpm). One spacing character is intentionally inserted to the left of bpm. The module also contains a PulseGenerator object and two auxiliary integer variables, initialized to 0.

The process function is pretty short, thanks to the use of the PulseGenerator object:

void AClock::process(const ProcessArgs &args) {
float BPM = params[BPM_KNOB].getValue();  period = 60.f * args.sampleRate/BPM; // samples
if (counter > period) {       pgen.trigger(TRIG_TIME);       counter -= period; // keep the fractional part  }
 counter++;  float out = pgen.process( args.sampleTime );  outputs[PULSE_OUT].setVoltage(10.f * out);  lights[PULSE_LIGHT].setBrightness(out);  }

In the process function, we first convert the bpm value to a period expressed in Hz, which tells us how distant the triggers are far from each other in time. A bpm value expresses how many beats are to be sent in a minute. For the sake of simplicity, our clock sends one trigger pulse for each beat. To get the number of pulses per second, we need to divide the bpm by 60 seconds. For instance, a bpm of 120 is equivalent to a frequency of 2 Hz (i.e. two beats per second). However, we need to convert a frequency to a period. By definition, a frequency is the reciprocal of a period. Remember that the reciprocal of a division is a division with inverted order of the terms (i.e. BPM/601=60/BPM ). Finally, the period is in seconds, but we are working in a discrete-time setting, where the sampling period is the basic time step. We thus need to multiply the seconds by the number of time steps in a second to get the total time steps to wait between each trigger. As an example, if the bpm is 120, the period is 0.5 s, corresponding to 22,050 time steps at a sampling rate of 44,100. This means that each 22,050 calls to the process function, we need to trigger a new pulse.

Back to the code – once the period is computed, we compare it against the counter, increasing by one unity at each time step. When the counter is larger than the period, a trigger is started by calling the trigger method of the PulseGenerator pgen. At this point, if we would reset the counter to 0, we would keep cumulating the error given by the difference between period and counter at each pulse. Instead, removing the period from counter (which is always greater or equal than period when we hit the code inside the if) enables compensation for this error, as discussed in the opening of this section.

At the end of the process function, we increase the counter and we connect the output of the pulse generator to the output of the module.

As promised, we will now discuss the use of lights and show how to obtain smooth transitions. The first way to turn the light on while the pulse trigger is high is to simply set its value, as we did in the previous module, using the setBrightness method.

There is one issue with this code, however. The pulse triggers are so short that we cannot ensure that the light will be turned on and be visible to the user. If the video frame rate is approximately 60 Hz (i.e. the module GUI is updated each 16 ms), it may happen that the pulse turns on and off in the time between two consecutive GUI updates, and thus we will not see its status change.

To overcome this issue, there a couple of options:

  • Instantiate a second pulse generator with trigger duration longer than 16 ms that is triggered simultaneously to pgen but lasts longer for giving a visual cue.
  • Use another method of the Light object, called setSmoothBrightness, which rises instantly but smooths its falling envelope, making it last longer.

The first option is rather straightforward but inconvenient. Let us discuss the second one. The setSmoothBrightness method provides an immediate rise of the brightness in response to a rising voltage and a slow fall in response to a falling voltage, improving persistence of the visual cue.

To invoke the setSmoothBrightness method to follow the pulse output, you can do as follows:

 lights[PULSE_LIGHT].setSmoothBrightness(out, 5e-6f);

where the first argument is the brightness value and the second argument scales the fall duration. The provided value gives us a nice-looking falling light.

Well done! If you are still with me, you can play with the code and follow on with the exercises. This chapter has illustrated how to move from an algorithm to its C++ implementation. From now on, most modules will implement digital signal processing algorithms of increasing complexity, but don’t worry – we are going to do this step by step.

Exercise 1

Try implementing a selector knob to choose whether the bpm value means beats per minute or bars per minute. The selector is implemented using an SvgSwitch subclass.

Exercise 2

A useful addition to the module may be a selector for the note duration that divides or multiplies the main tempo. Try implementing a selector knob to choose between three different note durations:

  • Half note. The clock runs at half speed.
  • Quarter note. Four beats in a bar, as usual.
  • Eighth note. The clock runs two times faster.

You can implement this switch using the three-state SvgSwitch.

6.6 Sequencer Module

TIP: In this section, you will learn about an important Rack object, the SchmittTrigger. This allows you to respond to trigger events coming from other modules.

An N-step sequencer stores several values, ordered from 1 to N, and cycles through them at each clock event. Values may be continuous values (e.g. a voltage to drive an oscillator pitch in order to create a melodic riff) or binary values to tell whether at the nth step a drum should be triggered or not. Of course, in modular synthesis, creative uses are encouraged, but to keep it simple at this time we will just think of a step sequencer as a voltage generator that stores N values and outputs one of them sequentially, with timing given by a clock generator.

A step sequencer is a simple state machine that transitions from the first to the last state and resets automatically at the last one. For each state, the module sends an output value that is set using a knob. The simplest interface has one knob per state, allowing the user to change any of the N values at any time.

In this section, we want to build a basic eight-step sequencer, with eight knobs and a clock input (see Figure 6.6). At each pulse of the clock, the state machine will progress to the next state and start sending out a fixed voltage equal to the one of the knob corresponding to the current state. Eight lights will be added to show which one is the currently active status.

Figure  6.6:  An eight-step sequencer seen as a state machine. Transitions between states are unidirectional and are issued by an input trigger. For each state, a sequencer module outputs the voltage value corresponding to the current state. Conventional sequencers have one knob per state, allowing for easy selection of the values to be sent to output. Please note that an additional reset button may allow transitions to happen from the current state toward the first one.

The challenge offered by this module is the detection of trigger pulses on the input. This can be simply done using the SchmittTrigger object provided by Rack. A Schmitt trigger is an electronic device that acts as a comparator with hysteresis. It detects a rising edge and consequently outputs a high value, but avoids turning low again if the input drops below the threshold for a short amount of time. The SchmittTrigger implemented in Rack loosely follows the same idea, turning high when the input surpasses a fixed threshold and turning low again only when the input drops to zero or below. It provides a process(float in) method that takes a voltage value as input. The method returns true when a rising edge is detected, and false otherwise. The object relies on a small state machine that also detects the falling edge so that the user can always check whether the input signal is still in its active state or has already fallen down, using the isHigh method.

Our sequencer module, ASequencer, will react to each rising edge at the clock input and advance by one step. The Module subclass follows:

Struct Asequencer : Module {
enum ParamIds {
    PARAM_STEP_1,
    PARAM_STEP_2,
    PARAM_STEP_3,
    PARAM_STEP_4,
    PARAM_STEP_5,
    PARAM_STEP_6,
    PARAM_STEP_7,
    PARAM_STEP_8,
    NUM_PARAMS,
};
enum InputIds {
    MAIN_IN,
    NUM_INPUTS,
};
enum OutputIds {
    MAIN_OUT,
    NUM_OUTPUTS,
};
enum LightsIds {     LIGHT_STEP_1,     LIGHT_STEP_2,     LIGHT_STEP_3,     LIGHT_STEP_4,     LIGHT_STEP_5,     LIGHT_STEP_6,     LIGHT_STEP_7,     LIGHT_STEP_8,     NUM_LIGHTS, };
dsp::SchmittTrigger edgeDetector; int stepNr = 0;
Asequencer() {     config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);     for (int i = 0; i < Asequencer::NUM_LIGHTS; i++) {          configParam(PARAM_STEP_1+i, 0.0, 5.0, 1.0);     } }     void process(const ProcessArgs &args) override;
};

As you can see, the knobs also provide storage of the state values.

The process function is as simple as follows:

void Asequencer::process(const ProcessArgs &args) {
if (edgeDetector.process(inputs[MAIN_IN].getVoltage())) {       stepNr = stepNr % 8; // avoids modulus operator }
for (int l = 0; l < NUM_LIGHTS; l++) {       lights[l].setSmoothBrightness(l == stepNr, 5e-6f); }
outputs[MAIN_OUT].setVoltage(params[stepNr].getValue()); }

The SchmittTrigger processes the input signal in order to reveal any trigger pulse. Whenever a rising edge is detected, the current step is increased, with the caveat that we need to reset it to 0 when we reach 7. We can do that using the C++ modulus operator “%” as shown above, or the bit manipulation trick shown in Section 2.13. You will find the latter in the actual implementation of the ABC plugins.

Exercise 1

Now that we have both a Clock and a Sequencer module, we can build musical sequences by driving the sequencer with a clock at a given bpm. Try building a simple musical phrase using the sequencer, the clock, and a VCO.

Exercise 2

It may be useful to reset the step counter at any time using a button or a gate input. Try implementing both using a SchmittTrigger. It will help to debounce the button and detect rising edges on the input signal.

Exercise 3

Try implementing a binary step sequencer, replacing knobs with switches, for programming drums. You can use the CKSS parameter type. It has two states, suiting your needs.

6.7 Binary Frequency Divider

TIP: In this section, you will learn how to encapsulate and reuse code.

Frequency-dividing circuits and algorithms are necessary to slow down a clocking signal (i.e. a periodic pulse train) to obtain different periodic clock signals. In digital electronics (and in music production too!), these are usually called clock dividers. Many usage examples of frequency dividers exist. Back in the analog era, DIN SYNC clocks had no standard, and to synchronize a machine employing 48 pulses per quarter note (ppq) with a slave machine requiring 24 ppq, one needed to use a clock divider with a division factor of 2. In modular synthesis, we can use a clock divider for many purposes, including sequencing, stochastic event generation, and MIDI synchronization (remember that MIDI clock events are sent with a rate of 24 ppq). Binary frequency-dividing trees are employed to obtain eighth notes, quarter notes, half notes, and so on from a fast-paced clock signal. For this reason, we may want to implement a tree of binary frequency dividers, which allows us to get multiple outputs from one single clock, with division factors of 2, 4, 8, 16, and so on. We may see this as an iterative process, where output[i] has half the frequency of output[i-1] (i.e. the same as implementing multiple factor-2 frequency dividers and cascading each one of them).

There are many C++ implementations out there of binary frequency-dividing trees even in the VCV Rack community. Some of them are based on if/else conditional statements. Here, we want to emphasize coding reuse to speed up the development. We will implement a clock divider by creating a struct implementing a factor 2 divider and cascading multiple instances of this, taking each output out.

We start with the implementation of a 2-divider object implemented as follows:

struct div2 {
  bool status;
  div2() { status = false; }
  bool process() {       status ^= 1;       return status;
  } };

To divide the clock frequency by a factor of 2, we can simply output a high voltage every second pulse we receive as input. In C++ terms, we may toggle a 25-Boolean variable status every two calls of its process() function. This is done by XOR-ing the status with 1.

The module, Adivider, has a bunch of div2 objects, one per output. Each div2 object is connected to a PulseGenerator, which in turn sends its signal to the output. Each divider feeds another divider, except for the last one, so that at each stage the toggling frequency slows down.

We also use the SchmittTrigger to detect edges in the input signal. In this case, we will activate at the rising edge of an incoming clock signal, initiating the process of going through our clock dividers.

This is done by a method that must be called recursively to explore the cascade of our div2 objects until we reach the last of our clock dividers or until the current divider is not active. In other words, if the current divider activates, it propagates the activation to the next one. The function is called iterActiv because it iterates recursively looking for activations.

The Module subclass is shown below:

struct ADivider : Module {
  enum ParamIds {
    NUM_PARAMS,
  };
  enum InputIds {
    MAIN_IN,
    NUM_INPUTS,
  };
  enum OutputIds {
    OUTPUT1, // this output will be hidden
    OUTPUT2,
    OUTPUT4,
    OUTPUT8,
    OUTPUT16,
    OUTPUT32,
    NUM_OUTPUTS,
  };
  enum LightsIds {     LIGHTS1,     LIGHT2,     LIGHT4,     LIGHT8,     LIGHT16,     LIGHT32,     NUM_LIGHTS,   };
  div2 dividers[NUM_OUTPUTS];   dsp::PulseGenerator pgen[NUM_OUTPUTS];
  void iterActiv(int idx) {     if (idx > NUM_OUTPUTS-1) return; // stop iteration     bool activation = dividers[idx].process();     pgen[idx].trigger(TRIG_TIME);     if (activation) {          iterActiv(idx+1);       }   }
  dsp::SchmittTrigger edgeDetector;
  ADivider() {     config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);   }
  void process(const ProcessArgs &args) override; };

Finally, we implement the processing with the cascade of div2 objects. Please note that we start the recursive iteration only when the SchmittTrigger activates on a clock edge. Each output is processed so that it gives a +10 V pulse if the corresponding PulseGenerator is high:

void ADivider::process(const ProcessArgs &args) {
  if (edgeDetector.process(inputs[MAIN_IN].getVoltage())) {             iterActiv(0); // this will run the first divider (/2) and iterate through the next if necessary   }
  for (int o = 0; o < NUM_OUTPUTS; o++) {       float out = pgen[o].process( args.sampleTime );       outputs[o].setVoltage(10.f * out);       lights[o].setSmoothBrightness(out, 0.5f);   } }

Before closing this section, we may want to note that the execution time of this implementation is quite variable as in some cycles there will be no call to iterActiv, while in some other cases this will be called recursively until the last output. For this module, we are not worried by this because the number of iterations is bounded by NUM_OUTPUTS and the function is quick to execute, but in DSP practice recursion is possibly dangerous. Always weigh the pros and cons and evaluate your code thoroughly. In your DSP development, take into account that one overly slow processing cycle may create a domino effect on other DSP modules or system functions, with unpredictable results. Suppose that you can implement the same algorithm in two ways: one with constant computational cost and another with variable computational cost. If the latter has a lower computational cost, on average, but with rare peaks of computing requirements, the first solution may still be better: the system will be more robust to unpredictable computing peaks. Of course, it depends a lot on the application, and experience will teach what is best.

A final remark about the panel graphics. Take a look at the background SVG that was created for ADivider. The use of a vertical spacing of 40 px was easy to accomplish in Inkscape, since the Shift+Down combination shifts the selected object by 20 px. Also, it is very easy to create vertical lines of 40 px length thanks to the ruler function in the status bar of Inkscape. This tells the angle and distance (i.e. the length) of your line.

Exercise 1

What if you want to reset all your counters (e.g. for a tempo change or to align to a different metronome)? You need a reset method in div2 to be called for each divider at once. You also need a button, and you need this button to be processed only at its pressure, using a SchmittTrigger. Your turn!

Exercise 2

There are a few lines of code we did not explore – those related to the widget. There is nothing new in these lines; however, I want you to take a small challenge now: since the number of outputs and lights is bounded by NUM_OUTPUTS, you may try to write the widget code with a for loop that instantiates a light and an output port for each of the items in the OutputIds enum. Of course, each port and light will have coordinates that are dependent on the iteration counter. Remember to connect each port and light to the correct item in the respective enum.

Exercise 3

We ended up coding a few lines to get a nice and useful module. Compare this module with other modules in the VCV Rack community performing the same function and look at their code. Try to understand how they work and make a critical comparison trying to understand in what regards this module is better and what could represent a disadvantage. Take into account computational cost considerations, reusability, code readability, memory allocation, and so on.

Exercise 4

The dsp namespace (see include/dsp/digital.hpp) has a ClockDivider struct. Try to create a clock divider tree module using this struct and compare it to the ADivider module.

6.8 Random Module

TIP: In this section, you will learn how to generate random numbers using Rack utility functions.

Random number generation is an enabling feature for a large array of compositional techniques, allowing unpredictable behaviors to jump in or even leaving the whole generation process to chance. Moreover, generating random numbers at sampling rate is equivalent to generating noise, and thus you can generate random numbers at sampling rate to create a noise generator. Finally, random numbers are used to randomize the parameters of your module or can be necessary for some numerical algorithms (e.g. to initialize a variable). For a recap on random signals and random generation algorithms, please refer to Section 2.12.

VCV Rack provides random number generation routines in its APIs, under the random namespace. These are:

  • void random::init(). Provides initialization of the random seed.
  • uint32_t random::u32(). Generates random uniform numbers in the range of a 32-bit unsigned int variable.
  • uint64_t random::u64(). Generates random uniform numbers in the range of a 64-bit unsigned int variable.
  • float random::uniform(). Generates random uniform float numbers ranged 0 to +1.
  • float random::normal(). Generates random float numbers following a normal distribution with 0 mean and standard deviation 1.

Please note that only the normal distribution has zero mean, and thus the other three will have a bias that is half the range. Of course, from these APIs provided by Rack, you can obtain variations of the normal and uniform distributions by processing their outputs. By adding a constant term, you can alter the bias. By multiplying the distributions, you change the range and, accordingly, the variance. Furthermore, by squaring or calculating the absolute value of these values, you get new distributions.

In this section, we describe how a random number generator module, ARandom, is developed to generate random signals at variable rate. Our module will thus have one knob only, to set the hold time (i.e. how much time to wait before producing a new output value). This is not unlike applying a zero-order hold circuit to a noise source (i.e. sampling and holding a random value for a given amount of time). However, equivalently, and conveniently, we can just call the random generation routine when we need it. The knob will produce, on one end, one value for each sampling interval, producing pure noise, or one value per second, producing a slow but abruptly changing control voltage. An additional control that we can host on the module is a switch to select between the two built-in noise distributions (normal and uniform).

The module struct follows:

struct ARandom : Module {
  enum ParamIds {
      PARAM_HOLD,
      PARAM_DISTRIB,
      NUM_PARAMS,
  };
  enum InputIds {
      NUM_INPUTS,
  };
  enum OutputIds {
      RANDOM_OUT,
      NUM_OUTPUTS,
  };
  enum LightsIds {       NUM_LIGHTS,   };
  enum {       DISTR_UNIFORM = 0,       DISTR_NORMAL = 1,       NUM_DISTRIBUTIONS   };
  int counter;   float value;
  ARandom() {       config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);       configParam(PARAM_HOLD, 0, 1, 1, “Hold time", “s");       configParam(PARAM_DISTRIB, 0.0, 1.0, 0.0, “Distribution");       counter = 0;       value = 0.f;   }
  void process(const ProcessArgs &args) override;  };

The ParamIds enum hosts the hold time knob and a distribution selector. We enhance code readability by adding another enum to the struct, associating the name of the two distributions to two numbers (i.e. the two values of the PARAM_DISTRIB switch). Two variables are declared, a float that stores the last generated value and an integer storing the number of samples since we last generated a random number. The latter will increase once per sample time, and be compared to the value to see if it is time to generate a new sample.

Before going into the details of the process function, let us consider the configParam for the hold time knob. The configured range goes from 0 to 1 second, for simplicity. Obviously, when the knob is turned to the minimum value, the hold time will be equivalent to one sample, for continuous generation of random values. On the other hand, the hold time will be equivalent to Fs samples.

We are getting now to the process function. This only has to read the parameters and let the random routines generate a new value, if it is time to do that. The code follows:

void ARandom::process(const ProcessArgs &args) {
  int hold = std::floor(params[PARAM_HOLD].getValue() * args.sampleRate);
  int distr = std::round(params[PARAM_DISTRIB].getValue());
  if (counter ≥ hold) {       if (distr == DISTR_UNIFORM)        value = 10.f * random::uniform();       else        value = clamp(5.f * random::normal(), -10.f, 10.f);       counter = 0;   }   counter++;
  outputs[RANDOM_OUT].setVoltage(value);  }

The routine first reads the status of the parameters. The number of samples to wait until the next number is generated is stored into the variable hold and is computed by multiplying the value of the knob by the sampling rate. The PARAM_HOLD knob goes from 0 to 1 seconds.

If the counter is equal to3 or greater than hold, the module generates a number according to the chosen distribution. When hold is 0 or 1, the module generates a new sample for each call of process.

Each distribution is scaled properly to fit the Eurorack voltage range: random uniform will be ranged 0 to 10 V, random normal will give random numbers around 0 V, possibly exceeding the −10 to +10 V range, and thus clamping it to the standard voltage range is a good idea.

The rest of the module is quite standard, so you can refer to the source code to see how the module is created.

Exercise 1

The hold knob maps time linearly in the range [0, 1]. The first quarter turn of the knob compresses most of the useful values. What about replacing time with tempo? By considering the value on a beats per minute (bpm) scale, we have a musically meaningful distribution of time values along the knob excursion range that provides finer resolution where it matters more. Specifically, the knob should display bpm values in the range 15 to 960. This can be done according to the following:

BPM=602v

(6.2)

where the knob value is v2,4 . This is easily achieved exploiting optional arguments of the Module::configParam method.

The process method should compute the values according to the above, and convert the bpm value into a time in samples according to the following:

hold|smp=60FsBPM=Fs2v

(6.3)

Such a module may be useful for generating random values at a steady tempo, but it is not generating noise anymore. One workaround to this would be to increase the upper bpm limit to a value that makes the hold time equal or shorter than one sample. Do consider, however, that the sample duration depends on the sampling rate, and thus one would have to adjust the upper limit if the engine sample rate is changed by the user. Unfortunately, Rack v1 APIs do not allow for this, and thus your best solution is to consider the worst case (highest sampling rate).

One last tip related to this example: change the strings in configParam to indicate that the knob is a tempo, not an absolute time.

Exercise 2

The module of Exercise 1 is synchronized to an internal bpm tempo. This is useful for generating rhythmic random modulations, but cannot synchronize with other clock sources. Modify the ARandom module to include an input port that allows synchronization to an external clock source. By checking whether the input port is connected, you can exclude the internal clock generation mechanism. This system is the equivalent of a sample and hold circuit applied to a random generator.

6.9 To Recap: Exercise Patches

Check the online resources to see how to complete these exercises.

6.9.1 Creating a Bernoulli Distribution and Bernoulli Gate

A Bernoulli distribution is a random distribution that only allows two values, 0 and 1. You can create that by using the ARandom module and the AComparator. The random numbers need to be compared against a threshold value (e.g. using LOG instrument “Precise DC Gen”). The higher the threshold, the lower the number of “1” outputs.

A Bernoulli gate is a binary multiplexer that sends the input to either of the two outputs. You can modify the AmuxDemux to let a CV in to select which output should be used and send the Bernoulli random values generated using AComparator to control the output port.

6.9.2 Polyrhythms

By using two sequencers driven by the same clock, you can generate interesting rhythmic patterns. We can connect AClock to two ASequencer modules, but they both would have eight steps for a 4/4 time signature. You can use ADivider to have them running at multiple rates. However, if you want to have one running with a 4/4 signature and the other one at a 3/4 signature, for example, you need to modify ASequencer to add a knob to select how many steps to loop (from one to eight), or have a Reset input and ensure it is triggered at the right time (after 3/4).

Notes

1    Please note, we are not going to place screws in the ABC modules for the sake of simplicity.

2    The period of a year consists of 365.24 days approximately.

3    Tip: In principle, the condition to trigger the generation of a new number should be counter == hold. However, this comparison may produce bugs if for some reason (e.g. an overrun due to preemption to Rack or a variable overwritten by accident by a buggy write access) we skip this code. From the next time on, the condition will be never verified, because counter will always be larger than hold and the algorithm will fail in generating a new random number. Thus, it is always better to use the equal to or greater than condition, to avoid such issues.

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

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