Interactive Swarm Space

How to code your own unit



Preparations

Writing your own units in C++ might seem like a scary task if you haven't had any experience with C++ yet. However, in this section we will try and help you in every way we can to get the necessary understanding to complete the task. Before you start writing the actual code, we strongly advice you to make some preparations and complete the decision making process we propose in the following paragraphs.

Of course it's vital to check if there isn't already a unit (or a combination or units) that performs your task. If no alternatives can be found, the search for other units is still not quite finished, because ideally, you find a relative of the unit you intend to develop, meaning a unit that has already some of the functionality you need in your unit. This unit will be your parent class. ANY unit in ISOSynth has a parent unit, most of them use one of the three following units as their parents: InputUnit, ProcessUnit or OutputUnit. The difference lies in the AudioPorts setup of these fundamental unit: While InputUnit (and all children thereof) are meant as input units to your patch and consequently have only an audio output port (over witch you will connect it with your patch) the actual input data is acquired internally (e.g. by generating a signal, or getting it from an external source, e.g. your ADC). OutputUnits are the opposite. In any patch, the OutputUnit is the last unit to connect to, therefore OutputUnits have only an audio input port. And finally, the ProcessUnits and its children, are signal processing units and have both an audio input and audio output. These three fundamental units are good starting points, but fairly often, they are not the best choice because there is a parent class that is even closer to your desired class, or in other words, it has more of the functions you need. Inheritance is an important and powerful programming paradigm, mainly but not exclusively because it let's you reuse code that already works, be it individual classes or whole frameworks. A good idea might be to familiarize yourself with the unit class tree in our Doxy gen Documentation and find a spot, where your unit could fit in. Of course looking at existing code is one of the best ways to learn how to develop your own.

Our next step will be to think about the control input of your unit. Surely you can program a unit, that does always the same thing, but it gets only really useful and interesting, when you add a certain amount of interactivity. This can be accomplished by adding ControlPorts and SwitchPorts to your unit - we have introduced them in our introduction section. Keep in mind that ControlPorts have a audio-rate resolution, for most cases this is not really necessary. SwitchPorts are being checked each frame buffer (typically all 512 samples), that's about a 1/10 of a second and still barely noticeable.

It's also wise to think in advance about the multichannel capability of your unit. What would you like to do with the separate channels: Do the same process parallel on all channels (true multichannel), share certain information among all channels or maybe even disallow creating more than one channel. If you are coding a filter for example, should the ControlPort that sets the filter coefficients control all channels together or separately. This has strong implications on the choice of internal data types. If all channels share a data type, this data type will be a simple int or float. Well actually, in ISOSynth we don't directly use floats, we use a typdef called sample. The reason for that is, we would like to keep the option of using a different floating-point built-in type at any later time (e.g. double). But if all channels need to have a separate entity of this data type, you will most probably use arrays, vectors or ISOs internal data collection type Frame. Here is an example:

Imagine you are coding a lowpass filter and you have a SwitchPort that lets the user change the threshold frequency. This frequency needs to be stored in the unit so that both the SwitchPort routine and the filter routine can access it. So your header file might say in its protected block:

sample mThreshold;  //store threshold frequency (same for all channels)

This is fair and square, but all channels of your filter unit will have the same threshold. Wouldn't it be much more convenient to have a threshold variable for all every channel. Your options are

sample* mThresholdArr;         // pointer to an array of threshold frequencies 
QVector<sample> mThresholdVec; // vector of threshold frequencies
Frame mThresholdFrame;         // ISO vector of sample values

While arrays are very fast and slim, they are prone to programming errors, mainly "array out of bounce" errors. Only use arrays, if you are sure what you are doing. Vectors are quite convenient, can grow over time and can be accessed like arrays (e.g. "mThresholdVec[3] = 0.0;"), but they are somewhat slower and need more memory than arrays. The best alternative (if you are using floating points) is a Frame, which is quite fast and used all across ISOSynth. All events for example are sent as Frames and are thus inherently multichannel-compatible, as are ControlPort or SwitchPort calls in general.

Before we start coding (yay!) we should remind you again of ISOSynths signal flow. It is crucial to understand, how buffers are passed from unit to unit in your patch:

Buffer Data Flow
Buffer Data Flow in a Patch

As you probably already know, ISOSynth (like any other software synthesis framework) puts audio data in buffers. This means, the signal flow will process a whole bunch of audio samples at once. The size of such a buffer is usually the same for all units (but it doesn't have to, the connections between two units will take care of the conversion where necessary). The default frame buffer size is 512 samples and it is stored in the variable mFrameCount that is part of every to unit because it's in fact a protected member of the base class "Unit", consequently all children inherit it. Back to buffers: Such a buffer is created by ISOSynth itself and initially passed to the OutputUnit(s), by calling their process() method with the pointer to the buffer as argument:

void process(Buffer* pBuffer) throw SynthException {
	Unit::process(pBuffer); // pass it up
	...
}

As you can see in the figure above, all Units, except InputUnits, will pass on the buffer pointer by calling the process method of their predecessor in a recursive manner. The InputUnit will then generate the first actual buffer content by completing its process method. When the end of the method body of InputUnit's process method is reached, the program will jump to the process method from which InputUnit's process() has been called, sort of retracing the steps of the recursive call tree. The process is shown in this pseudo syntax:

Synth::"create a new Buffer* pBuffer"
{
   OutputUnit::process(pBuffer) 
   { 
      ProcessUnit1::process(pBuffer) // pass up
      {
         ProcessUnit2::process(pBuffer) // pass up
         { 
            InputUnit::process(pBuffer) // pass up 
            {
               // ...write initial data to buffer 
            }
            continue ProcessUnit2::process() // manipulate Buffer
         }
         continue ProcessUnit1::process() // manipulate Buffer
      }
      output buffer in OutputUnit
   }
}

All units have an optional internal Buffer (mBuffer, also a member of iso_synth_unit, base class of all units), it can be used to store previous data or as a temporary Buffer in more complex process() calls.



Writing the Code

You probably best create your own directory in your synth_app XCode project called something like "my units". Right-click on this folder and select: "New File" -> Add... -> New File -> C and C++ -> C++ File. We have the naming convention, that all units should start with "iso_synth_", but that's optional. Look out: The Location path needs to be set! Otherwise your code will be placed in the project folder. Which will sooner or later create annoyances.

create a c++ file

As for the content of your newly created .h and .cpp files, again we'd like to point you to the existing units. But for the sake of a complete walkthrough, we are about to dissect one of the most easy-to-understand units in ISOSynth, the AbsUnit (see Math section). This should give you a practical insight into the process of coding a unit and at the same time the necessary theoretical basis.

Let's start with the header file. A header file holds the formal description of a class, e.g. what methods or variables does it contain, what's the visibility of it's members, who's the parent and so on. More on header files in Bruce Eckel's Thinking in C++. At first we'll list the whole header code and further down we'll explain individual chunks of it separately.

#ifndef _iso_synth_abs_unit_h_
#define _iso_synth_abs_unit_h_


#include "iso_synth/iso_synth_process_unit.h"
#include <iostream>
#include <cmath>

namespace iso
{

namespace synth
{

class AbsUnit : public ProcessUnit
{
public:
   static const String sClassName;
	
   AbsUnit();
	
   AbsUnit(const String& pName,
           FanInType pFanInType = FANIN_ADD);

   AbsUnit(unsigned int pChannelCount,
           FanInType pFanInType = FANIN_ADD);
	
   AbsUnit(const String& pName,
           unsigned int pChannelCount,
           FanInType pFanInType = FANIN_ADD);

   AbsUnit(unsigned int pFrameCount,
           unsigned int pChannelCount,
           unsigned int pRate,
           FanInType pFanInType = FANIN_ADD) throw (SynthException);

   AbsUnit(const String& pName,
           unsigned int pFrameCount,
           unsigned int pChannelCount,
           unsigned int pRate,
           FanInType pFanInType = FANIN_ADD) throw (SynthException);

   AbsUnit(const Unit& pUnit);

   AbsUnit(const String& pName,
           const Unit& pUnit);

   AbsUnit(const serialize::SerializeData& pSerializeData)
      throw (serialize::SerializeException);

   virtual ~AbsUnit();

   virtual serialize::SerializeData
      serialize(const String& pSerialName = sClassName) const;

   static serialize::Serializable*
      deSerialize(const serialize::SerializeData& pSerializeData)
      throw (serialize::SerializeException);

   void setOffset(Frame pValues);

   virtual void process(Buffer* pBuffer) throw (SynthException);
		
protected:

   void init();

   static unsigned int sUnitCount;

   Frame mOffsets;

};
}; // namespace synth
}; // namespace iso

#endif

The whole header is wrapped in a ifndef block with a unique (!!) name, this is to avoid that, headers are defined twice:

#ifndef _iso_synth_abs_unit_h_
#define _iso_synth_abs_unit_h_
...
#endif

Next in line are the include statements: We need to include the parent of our unit, as well as some standard C++ libraries (one for printing out data to the console and one to perform some basic math)

#include "iso_synth/iso_synth_process_unit.h"
#include <iostream>
#include <cmath>

As you will see next, we will pack our new class in the synth namespace which resides inside the iso namespace. Doing so, we are avoiding naming conflicts, those are nasty to catch bugs, when two data types share the same name. For example our beloved Frame type of ISO might collide with a type called Frame by a video editing framework. You get the picture, right?

namespace iso
{
   namespace synth
   {

      //
      // ...class definition goes in here...
      //

   }; // namespace synth
}; // namespace iso

And now the class definition. You can see that this class inherits from ProcessUnit. The class members (class-internal methods and types) are separated in public members and protected members. Protected members can only be accessed from within the object, while public methods are callable be other objects. More on that in a bit.

class AbsUnit : public ProcessUnit
{
   public:
      ...
   protected:
      ...
}

Let's first look at the public members. You probably noticed, that there are quite a lot of constructor methods (all these methods that start with the class name, like AbsUnit() ). Although you don't really have to implement the same constructors, sooner or later you'll be happy that you did - and what's more, the implementation of these constructors is very straight forward as you'll see in our .cpp File further down.

// default constructor, uses default values for members (e.g. channelcount = 1)
AbsUnit();

// parameterized constructor: lets the caller define a name for his object
// as well as an optional FanInType	
AbsUnit(const String& pName, FanInType pFanInType = FANIN_ADD);

// another param. constructor suitable for multichannel AbsUnits (pChannelCount > 1)
AbsUnit(unsigned int pChannelCount, FanInType pFanInType = FANIN_ADD);
	
// combination of the last two parameterized constructors
AbsUnit(const String& pName,
        unsigned int pChannelCount,
        FanInType pFanInType = FANIN_ADD);

// custom frame count and sampling rate
AbsUnit(unsigned int pFrameCount,
        unsigned int pChannelCount,
	      unsigned int pRate,
	      FanInType pFanInType = FANIN_ADD) throw (SynthException);

// another combination of parameterized constructors
AbsUnit(const String& pName,
        unsigned int pFrameCount,
        unsigned int pChannelCount,
        unsigned int pRate,
        FanInType pFanInType = FANIN_ADD) throw (SynthException);

// copy constructor, will create a copy of an existing unit,
// notice: it's not AbsUnit(const AbsUnit& pUnit)! So the AbsUnit-specific
// members are hidden and cannot be copied.
AbsUnit(const Unit& pUnit);

// another copy constructor, but with a custom unit name
AbsUnit(const String& pName, const Unit& pUnit);

// constructor by serialization data; can be ignored at the moment
AbsUnit(const serialize::SerializeData& pSerializeData)
   throw (serialize::SerializeException);

// destructor
virtual ~AbsUnit();

Notice how the destructor ~AbsUnit() as well as some other methods have the keyword "virtual" prepended? When inheriting from another class, you not only inherit its types (like mBuffer, mFrameCount and so on), but also its public and protected methods! With the keyword virtual you make sure that not the parent's method is executed but your own! However this is only necessary for methods other than constructors.

Similarly to the constructor, which is automatically called when an object is initialized (e.g. AbsUnit* my_unit = new AbsUnit() <--- calls the appropriate constructor AbsUnit() ), a destructor is called, when an object is destroyed with the delete statement (e.g. "delete my_unit"). That's the nasty thing about pointers, you have to clean up after them. As you can see in Bruce Eckel's tutorial, the whole subject of pointer allocation and destruction is a little tricky, but as a rule of thumb think of this: Whenever you allocate a pointer using the "new" statement (AbsUnit* my_unit = new AbsUnit() ) you will have to delete it at some other point, preferably in the destructor. However sometimes, you don't want to allocate pointers at all, because some other object will pass you an object, who's memory address you'll use for your pointer, sharing the value that is stored at this memory location. Just to give you a feeling for pointers and destructors, the difficult bit in such a situation is of course: Who is responsible for cleaning up a shared pointer... But here we are already knee-deep in C++ mud, so let's keep it simple and move on with our header file.

Let's look at the rest of the public members:

// public method to set the offset values of this unit,
// not inherited, so no "virtual" is necessary here
void setOffset(Frame pValues);

// public process unit, again "virtual" is necessary,
// otherwise ProcessUnit's process() method would be called!
virtual void process(Buffer* pBuffer) throw (SynthException);	

There it is, our cherished process() method. The process() method can throw a SynthException. It's sort of a safety handle for difficult operations by sandboxing them in a "try - catch" code block, you'll see that in our cpp file. Check the "Thinking in C++" section about Exceptions if you desire to know more, however at this point it's not crucial to know more than what you can grasp from our examples here.

All methods other than constructors and destructors need to declare a return type, that's the value or object that is being passed back to the caller of this method. Here, the return type is "void", meaning: This method won't return anything.

Now for the protected members of AbsUnit:

protected:
// initialize members, init() should be called in each constructor
void init();
	
// count how many objects have been created of AbsUnit
static unsigned int sUnitCount;

// offset value for each channel. Remember our mThreshold example
// in the "Preparations" section 
Frame mOffsets;

That's it, our header file is complete. We've got everything declared that our class AbsUnit needs: The constructors together with the init() method will construct the new object, setOffset() will set the only synthesis control parameter of this class (mOffsets) and finally, the process() method that manipulates the process buffer handled to it.

Now we need to write the .cpp File, which holds the actual implementation of the mere descriptions written in the Header-File (iso_synth_abs_unit.h).

#include "iso_synth_abs_unit.h" //include header file

using namespace iso;	//use iso namespace
using namespace iso::synth; //... and use synth namespace

unsigned int AbsUnit::sUnitCount = 0;  // static member sUnitCount from the header file
                                       // is initialized here

// sClassName is a member of Unit, so even though it's not mentioned in the header file,
// we can set it here.
// it's a const, meaning, once we've set it, it cannot be changed.
const String AbsUnit::sClassName =
      serialize::SerializeManager::get().registerSerializable<AbsUnit>("AbsUnit"); 

AbsUnit::AbsUnit()  // our default constructor
   : ProcessUnit(String("%1_%2").arg(sClassName).arg(sUnitCount + 1))
   , mOffsets(mChannelCount)
{
   init();
   sUnitCount++;
}

AbsUnit::AbsUnit(const String& pName, FanInType pFanInType)
   : ProcessUnit(pName)
   , mOffsets(mChannelCount)
{
   init();
   sUnitCount++;
}

AbsUnit::AbsUnit(unsigned int pChannelCount, FanInType pFanInType)
   : ProcessUnit(String("%1_%2").arg(sClassName).arg(sUnitCount + 1), pChannelCount)
   , mOffsets(mChannelCount)
{
	init();
	sUnitCount++;
}
	
AbsUnit::AbsUnit(const String& pName, unsigned int pChannelCount, FanInType pFanInType)
   : ProcessUnit(pName, pChannelCount)
   , mOffsets(mChannelCount)
{
	init();
	sUnitCount++;
}

AbsUnit::AbsUnit(unsigned int pFrameCount, unsigned int pChannelCount,
                 unsigned int pRate, FanInType pFanInType) throw (SynthException)
   : ProcessUnit(String("%1_%2").arg(sClassName).arg(sUnitCount + 1),
                 pFrameCount, pChannelCount, pRate)
   , mOffsets(mChannelCount)
{
	init();
	sUnitCount++;
}

AbsUnit::AbsUnit(const String& pName, unsigned int pFrameCount,
                 unsigned int pChannelCount, unsigned int pRate,
                 FanInType pFanInType) throw (SynthException)
   : ProcessUnit(pName, pFrameCount, pChannelCount, pRate)
   , mOffsets(mChannelCount)
{
	init();
	sUnitCount++;
}

AbsUnit::AbsUnit(const Unit& pUnit)
   : ProcessUnit(String("%1_%2").arg(sClassName).arg(sUnitCount + 1), pUnit)
   , mOffsets(mChannelCount)
{
	init();
	sUnitCount++;
}

AbsUnit::AbsUnit(const String& pName, const Unit& pUnit)
   : ProcessUnit(pName, pUnit)
   , mOffsets(mChannelCount)
{
   init();
   sUnitCount++;
}



void 
AbsUnit::init()
{
   createSwitchPort("offset",
                    mChannelCount,
                    static_cast<void (Unit::*)(Frame)>(&AbsUnit::setOffset)););

   for (unsigned int c = 0; c < mChannelCount; ++c)
   {
      mOffsets[c] = 0.0; //default
   }
}	
	
void
AbsUnit::setOffset(Frame pValues)
{
	for (unsigned i = 0; i < mChannelCount; ++i)
	{
		if (i >= pValues.channelCount()) break;

		if (pValues[i] == NAN) continue;
		
		mOffsets[i] = pValues[i];
	}
}	
	
	
void 
AbsUnit::process(Buffer* pBuffer) throw (SynthException)
{
	Unit::process(pBuffer);
	
	if(mActive == false) return;
	
	sample* data = pBuffer->data();
	
	sample offset;
	
	for(unsigned int channel = 0, index = 0; channel < mChannelCount; ++channel)
	{
		offset = mOffsets[channel];
		for(unsigned int frame = 0; frame < mFrameCount; ++frame, ++index)
		{
			data[index] = fabs(data[index]-offset)+offset;
		}
	}
}

Again constructor implementations take up a lot of space, instead of looking at the individually, let's look at just one:

AbsUnit::AbsUnit(const String& pName,
                 unsigned int pFrameCount,
                 unsigned int pChannelCount,
                 unsigned int pRate,
                 FanInType pFanInType) throw (SynthException)
   : ProcessUnit(pName, pFrameCount, pChannelCount, pRate) // call constructor of parent!
   , mOffsets(mChannelCount)
{
   init();
   sUnitCount++;
}

You should already be familiar with the first line from our header file. Don't forget to prepend the class name plus "::" to each method (the constructor is a method too).

We see the start of the initialization list (highlighted in green): First of all, the constructor of our parent unit (ProcessUnit) is called, so that it may initialize all the parameters we inherited from it. The only difference to our parent is our class-specific Frame mOffsets, which we need to initialize separately on the next line. If there were other initializations to be done, the would take up another line before the {} method body. See the pitfalls section bellow for hints on initialization lists.

Inside the constructor method body, delimited by curly brackets, we see the content of this constructor, which is for ISO units usually quite sparse, because all class-specific member initializations are either done in the initialization lists or in the init() method, which we conveniently call from within each constructor. We could in fact write the content of the init() Method in each constructor and achieve the same result but with a lot of duplicated code! And beware the unholy wrath of duplicated code!! If you change something in one duplicate, you are likely to forget, that there is a copy of it somewhere else in your code. A sure recipe for evil lurking bugs *shiver*.

sUnitCount++ will simply increase the counter, this is actually mainly used to create objects with unique internal names, as you can see and some of the other constructors:

String("%1_%2").arg(sClassName).arg(sUnitCount + 1)
   // creates a unique name for each object

The init() method is quite easy to understand:

void 
AbsUnit::init()
{	
   createSwitchPort("offset",
                    mChannelCount,
                    static_cast<void (Unit::*)(Frame)>(&AbsUnit::setOffset));
	
   for (unsigned int c = 0; c < mChannelCount; ++c)
   {
      mOffsets[c] = 0.0; //default
   }
}

Unlike our constructors (and destructors), normal methods need a return type (value or object that is passed back to the caller of a method) or the keyword "void" that signifies that no value is returned. Inside the the method body we only do two things:

  • Create a new SwitchPort called "offset", which is multichannel-capable and linked to the setOffset() method
  • initialize all sample values inside our frame mOffsets with 0.0

Don't be bothered by the complex instruction of createSwitchPort(). Simply fill in your own code. The important thing is that the SwitchPort has to know which method to call. And we were just about to write the implementation of this method anyway:

void
AbsUnit::setOffset(Frame pValues)
{
   for (unsigned i = 0; i < mChannelCount; ++i)
   {
      if (i >= pValues.channelCount()) break;

      if (pValues[i] == NAN) continue;

      mOffsets[i] = pValues[i];
   }
}

We have a naming convention that all objects and values that are being passed to the object as parameters start with a p (for parameter), while all member variables start with an m. This is to avoid confusion amongst the two.

This setOffset() Method actually holds just a for-loop. This for-loop assumes the Frame pValues passed to it contains the same number of channels as the unit itself. However, we cannot be certain of this, and therefore the first line in the for-loop will terminate it, if the incrementor index i is greater or equal than the channel count of pValues. In order to be able to just set a specific channel's offset value while leaving the other channels as they are, we can send a Frame to setOffset (or to the SwitchPort "offset" for that matter) that contains only the value NAN ("not a number") except for the very channel you would like to set. Here is an example:

myAbsUnit->set("offset", Frame(5, NAN, NAN, NAN, 0.78, NAN));
   // only sets the 4th channel to 0.78 

on a side-note: We did in fact call the SwitchPort "offset" in with this set() method call, and because it's linked to the setOffset() method, it's equivalent to directly calling setOffset:

myAbsUnit->setOffset( Frame(5, NAN, NAN, NAN, 0.78, NAN) );
   // only sets the 4th channel to 0.78 

if the for-loop encounters a NAN, the loops is not terminated, but simply the execution jumps to the next iteration with the keyword continue.

And finally, if all goes well, the internal mOffset Frame is being supplied with the new parameters inside pValues. At this point you could also perform boundary checks, value transformations or whatever monkey voodoo magic you are willing to inflict upon your parameter values in order to fit it inside the desired value range for internal control frame.

Last but not least, there is the implementation of the most important part of your unit, the process() method. Writing really good process() methods is an art in itself, because they need to be as optimized as possible, but still very transparent to read. A lot of the pitfalls of writing process() method code will be mentioned in the next section. Fortunately our example code, isn't too complex though:

void 
AbsUnit::process(Buffer* pBuffer) throw (SynthException)
{
   Unit::process(pBuffer);

   if(mActive == false) return;

   sample* data = pBuffer->data();

   sample offset;

   for(unsigned int channel = 0, index = 0; channel < mChannelCount; ++channel)
   {
      offset = mOffsets[channel];
      for(unsigned int frame = 0; frame < mFrameCount; ++frame, ++index)
      {
         data[index] = fabs(data[index]-offset)+offset;
      }
   }
}

As explained in our "Preparations" Section above, we start our process() method by delegating the process() to our predecessor. The base class "Unit" will take care of that. As soon as every unit above our AbsUnit has completed their process() method (i.e. reached the end of the method body), the execution will return to our code and continue with the remaining statements. The first of which is the check, if the unit is active at all, because if not, the method execution will end here and leave the buffer untouched (sort of a bypass operation).

if(mActive == false) return;

Next, we need to make the buffered data more accessible. This can be done by a sample* (sample pointer) which is in fact an array. This makes it possible for data access inside the following nested for-loops to be as speedy as possible.

sample* data = pBuffer->data();

The sample value offset is merely a helper variable, and an attempt to further speed the for-loops. Since accessing local variables like offset is faster than accessing an element in an array, vector or frame (here: mOffset), and the fact that we do access offset twice inside each nested loop, there is a little bit of performance overhead to be avoided here.

What's more, "channel" is an index of the outer loop, so by using a helper variable, we reduce the amount of array access from (2 times the frameCount = 1024) per channel to 1! When writing your code, pay close attention to these indices and where they are used to access array-like constructs. Because if it's a nested for-loop, even the tiniest overhead like accessing an array instead of a local variable, can have an impact on your unit's performance. More hints about performance optimization can be found in the "Pitfalls" section.

Speaking about for-loops, notice how we have three distinct indices in our two nested loops: channel, frame and index. While channel is dedicated to the outer loop and frame to the inner loop, index is a thing in between, declared in the outer loop but incremented in the inner loop. This is because earlier, we have parsed the buffer data as a sample one-dimensional array. So all channels and frames of our buffer are being flattened into one string of samples. All the samples of the first channel, followed by the second channel, third, and so on. And that's why we need a separate index value here, for accessing our data array containing the buffered audio data. At this point, the typedef sample does not only mean float, but quite literally it means an audio sample (which is a float value anyhow).

The actual algorithm of our process() method, is a one-liner using the fabs() method from the standard library cmath (the reason why we had to include <cmath> in our header file). That will do it's job, write the absolute value of the buffered audio data back into the buffer, channel by channel, frame by frame. The offset as explained in the AbsUnit section of our Math chapter is merely a convenience to set a different signal mirroring point.



Pitfalls

Casts

Avoid casts inside the for-loops, especially casts from Integer to Float, like "v = (float) i;" or "v = static_cast<float>(i)". In some cases it makes sense for this exact reason to store an array of integer values as a floating point array.

Conditional statements and loops

try to reduce the amount of if-then-else or even worse for-loops you have in your processing loops. Here are some workarounds:

Instead of the following code

if (x > z)  
	z = x;   

write

(x > z) && (z = x); 

Confusing? These two statements are connected by a conditional AND operator, however the second statement will only be executed if the first statement is TRUE. Nice one, isn't it?

If you run into a situation where you have to create another for-loop inside the already nested two for loops, please, really make sure there is absolutely no way to avoid it. Try for example to think of a way you can use the two existing for-loops to take care of it somehow.

Division

Division is costly. Period. Never divide inside a for loop. However there is a trick widely used in programming complex systems. Instead of dividing by a certain value d, what you do is, you actually multiply by the inversion of d, which you can precalculate as a member variable.

So instead of writing:

x = a / static_cast<float>(mFrameCount); //cast and division = ouch!

you can write

x = a *  mInvFrameCount; //much speedier! 

where the float value mInvFrameCount has been set once in the init() method. Of course your header file has to declare mInvFrameCount as well.

Array or Vector Access

Try to reduce the amount of times you access an array or (worse) a vector. In our example above, we reduced access of our mOffsets Frame from 1024 to 1, by using a local variable offset, and setting offset only once per channel in the outer for-loop.

for(unsigned int channel = 0, index = 0; channel < mChannelCount; ++channel)
{
	offset = mOffsets[channel];
	for(unsigned int frame = 0; frame < mFrameCount; ++frame, ++index)
	{
		...//use offset here instead of mOffsets

Accessing Buffers

Don't access the buffers directly inside the for-loops, instead parse the content to a sample* (array of samples). Pointers are tricky beasts, if you are not used to work with them, it will take some time. Again Bruce Eckel's "Thinking in C++" is a very good starting point to learn about pointers.

Namespaces

By settling our class inside the nested iso::synth namespace, we can address all other namespace members simply by their name. However the iso namespace holds a variety of other namespaces like: iso::event, iso::flock, iso::midi and so on. We have to address them by their namespace + class name. A new EventTarget (iso::event::EventTarget) would therefore be allocated like this:

iso::event::EventTarget* my_target = new iso::event::EventTarget();

and because our class is already a member of the iso namespace (iso::synth::MyClass), we can omit iso:: and write:

event::EventTarget* my_target = new event::EventTarget(); 

Initialization Lists

The initialization list should be part of every constructor. In it, you call the constructor of your parent class and initialize some of your members. Which ones?

References (e.g. Frame& myFrameRef) need always to be referenced in the initialization list! All other members don't have to, but if you don't the default empty constructor for them is automatically called - which is often something you don't want.

What members should not be in the initialization lists?

In case you need to do a complex initialization, like calculating the values of the constructor parameters, the initialization list is not the place to do it. In fact, you can't do it. So it's better to do that inside the constructor or even better in the init() method (in case it's something that all constructors need to do anyhow).


Last updated: May 15, 2024