←Back to Redwood Audio DSP home

JUCE 3.x for VST Plugin Development (using JUCE 4.x?)

Need Help with this Tutorial? (Contact Us)

Was this useful?  (Consider a Contribution)

Download Final Source Code including built VST

The following notes represent a few additional topics we find it useful to be mindful of while working on VST plug-ins within the JUCE framework.  While they are not needed for illustration in the tutorial, your particular project may benefit from taking inspiration or implementing these features more completely.

 

 

1. VST Mechanisms through the JUCE Framework

Sample Rate - for plug-ins with time or frequency-dependent DSP elements, it is necessary to initialize the host rate or react to changes.  JUCE uses a simple global function for tracking sample rate:

double SampleRate_Hz = juce::getSampleRate();

 

It is important to consider that use is host specific and the only time you can assume this function is valid is within calls to your plug-in's processBlock.  Otherwise, it can return 0 or other invalid values.  Standard practice would be to maintain a status variable with the current rate (or a safe default).  At the beginning of each processBlock call, check the sample rate against the status variable and do appropriate handling for stable changes to the current rate.

 

VST Input/Output Pins - IO pins are the mechanism of hooking up your VST plug-in to the outside world.  They are by and large handled sufficiently by the default implementations and easy to overlook in context of the processBlock call - but a few "gotchas" are lurking here depending on the host and framework version.  For example, there are functions to getNumInputChannels() and getNumOutputChannels() and these can be safely used inside your processBlock call to check the number of IO buffers you have for use.  There is however, no guaranty that this is the number of IO pins truly connected to the outside world.  And if you have special processing for mono and stereo modes it is possible you are being tricked by your host (or this "quirk" of the current JUCE framework).  Depending on your needs, you could include a mode variable or work around this with the methods described in this thread.  We suspect future versions of JUCE may handle this better - stay tuned.

 

On a related note, you may want to tell your host what your input/output pins do.  There is a working mechanism for this and it is easy to use.  Simply, replace the default implementations of getInputChannelName(int channelIndex)  and getOutputChannelName(int channelIndex) in PluginProcessor.cpp.  In hosts such as AudioMulch the results can be dynamic for the context or mode your plugin operates in (shown when hovering the pin).       

 

 

2. JUCE Specific Helpers

JUCE String Tokenizer - the JUCE class for Strings has some handy conversions built in which makes it very easy to handle data ↔ String conversions and tokenize large data sets.  The main application we like this for is automatic setting and getting of state information to our VST user parameter list (an array of floats).  However, it is also very helpful for debugging etc..  Consider the following two functions which can be added anywhere to enable conversions between float arrays and juce::String.

String FloatArrayToString(float* fData, int numFloat)
{//Return String of multiple float values separated by commas

String result="";
if(numFloat<1)

return result;

for(int i=0; i<(numFloat-1); i++)

result<<String(fData[i])<<",";//Use juce::String initializer for each value

result<<String(fData[numFloat-1]);
return result;

}
int StringToFloatArray(String sFloatCSV, float* fData, int maxNumFloat)
{//Return is number of floats copied to the fData array
//-1 if there were more in the string than maxNumFloat

StringArray Tokenizer;
int TokenCount=Tokenizer.addTokens(sFloatCSV,",","");
int resultCount=(maxNumFloat<=TokenCount)?maxNumFloat:TokenCount;
for(int i=0; i<resultCount; i++)//only go as far as resultCount for valid data

fData[i]=Tokenizer[i].getFloatValue();//fill data using String class float conversion

return ((TokenCount<=maxNumFloat)?resultCount:-1);

}

 

If we have defined these (say in PluginProcessor.h), then our state information calls can take the form below and all updates to the parameter list are automatically included in our state information of the plug-in:

void StereoWidthCtrlAudioProcessor::getStateInformation (MemoryBlock& destData)
{//Save UserParams/Data to file
//Make sure public data is current (through any param conversions)

for(int i=0; i<totalNumParam;i++)

UserParams[i]=getParameter(i);

XmlElement root("Root");
XmlElement *el = root.createNewChildElement("AllUserParam");
el->addTextElement(String(FloatArrayToString(UserParams,totalNumParam)));
copyXmlToBinary(root,destData);

}

void StereoWidthCtrlAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{//Load UserParams/Data from file

XmlElement* pRoot = getXmlFromBinary(data,sizeInBytes);
float TmpUserParam[totalNumParam];
if(pRoot!=NULL)
{

forEachXmlChildElement((*pRoot),pChild)
{

if(pChild->hasTagName("AllUserParam"))
{

String sFloatCSV = pChild->getAllSubText();
if(StringToFloatArray(sFloatCSV,TmpUserParam,totalNumParam)==totalNumParam)
{//We have a new set, set with any conversions (via setParameter)

for(int i=0; i<totalNumParam; i++)

setParameter(i,TmpUserParam[i]);

}
else{};//ignore... or you could also call a "setUserParamDefaults" if desired.

}

}
delete pRoot;
UIUpdateFlag=true;//need to pass any changes on to our UI

}

}

 

OK, so now you have a setStateInformation and getStateInformation for your plugin that does not require any maintenance.  If you add or delete parameters, you only need to update the set/getParameter functions and related UI in your GUI. 

 

Some other good applications of the tokenizer are for debugging or user messaging.  As you may have noticed, it is not as easy/stable to debug real-time VST plug-ins as it is a standard application function (which can more easily be stepped through).  There is always the standard methods of updating a text Label in your UI, but this has its limitations.  The following are some other good mechanisms built into JUCE to help us out with the debugging process.

 

JUCE AlertWindows works like an MFC MessageBox but they can be called from anywhere within your code.  This makes it very handy for debugging or generating user messages, prompts and controls.  For advanced user controls, you can create an AlertWindow object (or DialogWindow) and spawn it with custom JUCE UI components (see full documentation here).  But for the most basic messaging, you can call them directly similar to AfxMessageBox.  The syntax is:

 

AlertWindow::showMessageBox(AlertWindow::InfoIcon,"Title text","text to display in box");

 

Where the "Title text" is what will be displayed in the title for the window, and the other string is your text to display - say the output of the FloatArrayToString function we made above or a user message with debug information.  The icon can be changed to the various types defined in AlertWindow (NoIcon,QuestionIcon,WarningIcon, or InfoIcon).

 

Writing to File with JUCE - Another useful step to debug might be to dump your data to a file.  JUCE has some reasonably simple helper classes for this (see full documentation here).  You can create a stream writer for continued output in byte form.  But a quick file can be made from a juce::String as follows:

File fileName("myfile.txt");

fileName.create();

fileName.replaceWithText("Hello World!");

 

If you want to use a file chooser dialog to come up with the path name - just add the following built in methods:

FileChooser ChooseFile("Save Data As:",File::getSpecialLocation(File::userHomeDirectory),"*.txt");
if(ChooseFile.browseForFileToSave(true))
{

File fileName2=ChooseFile.getResult().withFileExtension("txt");//make sure you end with desired extension
fileName2.create();
fileName2.replaceWithText("Hello World!");

}  

 

One more related File function which we find handy is the File::getNonexsistentChildFile member function.  This allows the creation of a series of related files.  For example, you can use the chooser to select the directory or base name for output.  Then, create a series of files based on that name (without writing over existing copies) using a specified pre and postfix.  See full documentation here.

 

 

3. Handy Audio DSP Helpers and Code Practice

There are a few things you can do in general to make your life of VST and audio dsp programming easier.  Maybe the easiest to ignore, but most helpful for you long term is to develop audio dsp code independent from your VST - view your work with JUCE and VST as a wrapper for your audio algorithm.  It is also very useful to really approach things in an object oriented way and build a library of elemental audio dsp classes (delays, EQ filters, etc.). 

 

Developing your audio algorithms in this way will not always seem the most optimal, but it will remain functional, readable and reusable.  Hopefully, each block along the way is tested and vetted, becoming something you can rely on.  Above all else, approaching it this way means you can always pull your core audio dsp code from the VST and exercise it through an easier interface than a third-party host.  You can always make an optimizing port after everything has settled - but once audio code gets ugly and something "sounds off", it is not the easiest thing to go back through and debug.

 

Everyone will have their own style for how they like fundamental blocks (such as delay lines) or process calls to look - the trick is to be consistent and complete.  The following is a summary of useful practice shaped like a high level generic audio dsp class.

 

//full class documentation including use and verification status if applicable...
class GoodAudioClass
{
public://construction

GoodAudioClass();//set any pointers to NULL, call getDefaults on mParam and set both status flags true
~GoodAudioClass();//call release();

public:

//enums  (for anything past mono/stereo algorithms, this should include channel IO)
enum InputIndex{LeftIn=0, RightIn,/*...,*/numInputs};
enum OutputIndex{LeftOut=0, RightOut,/*...,*/numOutputs};

 

////parameter interface////

struct Param

{//define all parameters in one easy to use structure, label units so you don't mix gain_lin and gain_dB etc.

float Fs_Hz, param2,  param3;

bool bypass;

};

int setParam (goodAudioClass::Param newParam);

/*Set parameters copies to a "safe"/buffered parameter struct while performing error checking and correcting for invalid settings, sets mNeedsUpdate flag to true and returns a warning code if changes were made*/

bool getParam (goodAudioClass::Param* theParam);

 

//If the pointer is good copy mParam to  theParam and return true, else return false

bool getDefaultParam (goodAudioClass::Param* theParam);

//If the pointer is good, load theParam with sensible defaults and return true, else false

 

//Optionally include quick settings (internal functions should use the same system of "safe" parameters

//Don't forget any needed error checking for invalid settings - bypass is safe...

void setBypass(bool bypass){mParam.bypass=bypass; };
bool getBypass(void){return mParam.bypass;};


////Processing interface////

int process (int NumSamples, float** SamplesIn, float** SamplesOut=NULL);

/*If it can be in-place processing – do that You can always flash copy to out and process there if they want clean input data.. order should be:

1.  If(mNeedsUpdate) updateData();

2. If(mNeedsFlush) clear();

3. Implement your Amazing Audio Idea.. */

float clockProcess (float inSamp); //Same as process for a single channel one step at a time

void flush(); //Simply sets the mNeedsFlush flag,

 


private://Helper functions and internal data

bool mNeedsUpdate, mNeedsFlush; //status flags to track need for updates or flush
GoodAudioClass::Param mParam;//Safe / Buffered parameters -- keep them valid at all times


//Actual active data used by processing
bool m_bypass;

float mFs_Hz, mP1, mP2; //actual internal members (can use target / current for interpolation)

 

//Optional buffer pointers etc.

float** internaldata;

 

//internal helper functions - so you only have to do things in one place!

bool updateData();/*translate safe parameters to internals. Minor changes are direct, major changes can call flush or allocate as needed */

bool allocate();//call release, do required allocations, then call clear

bool clear(); //set buffers to zero, reset index pointers, and flush subcomponents - set mNeedsFlush = false!

bool release();//release all dynamic memory (checking for NULL) and set pointers back to NULL


};//end GoodAudioClass;

 

 

Have any other good tips we should share or comments about this tutorial- please feel free to share them with us!