←Back to Redwood Audio DSP home

JUCE 4.x for VST Plugin Development (old JUCE 3.x Tutorial)

Need Help with this Tutorial? (Contact Us)

Was this useful?  (Consider a Contribution)

Download Tutorial Source Code including built VST or the Source using AudioParameterX Classes

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.

 

Bus Configurations - It seems like some features and best practices are still in flux for the current version.  For now, it is worth checking out the ROLI videos and discussion started in the forum:   "Revised Multibus API"

 

Float/Double Processing - while many standard piplines involve audio samples being passed through processing stages as single precision float (or even fixed point formats), there are cases where double precision can greately improve accuracy.  For example, single prceision errors in triginometric functions can create aliasing in synths, or worse, instability for filter designs at higher sample rates resulting in nasty surprises!  If you already utilize double precision processing internally, it might be nice to offer the double format directly to compatible hosts.  This is now possible by adding a few overrides to your AudioPluginProcessor.  The key functions are the override for the double precision version of processBlock, the query method  supportsDoublePrecisionProcessing, and the status check with getProcessingPrecision

 

To use support this, you must also implement the standard floating point processBlock for hosts which do not utilize it.  Once you have implemented both versions, you override supportsDoublePrecisionProcessing to return true;  The host then decides which to use - your only indication is which processBlock function has been called.  If you need special initialization for each version, you can attempt to query using getProcessingPrecision during your prepareToPlay method - However, it is always safer to be ready or detect and adjust to changes.     

 

If double it critical for your plugin, it maybe best to just make your DSP modeul class using double precision processing and make a light wrapper with float conversion in the corrisponding processBlock.  

      

Plugin Delay Compensation (PDC) - For plugins wishing to report their latency to be factored into the PDC of the host, or hosts which need the plugin latency to do their own adjustments.  Juce provides a pair of functions for getting/setting the current plugin latency.  To report the amount of delay applied by your plugin, simply call setLatencySamples from your plugin processor.

 

MIDI Controls / OSC- In many cases it is useful to talk to the host or other plugins via MIDI.  You may have wondered about the midiMessages buffer in your plugins processBlock.  If in the Projucer you ticked "Plugin wants MIDI input" or "Plugin produces MIDI output" - this buffer is for you!  Juce provides a MidiBuffer::iterator class to step through any messages and check for controls.  Posting controls are just as easy.  The following code shows an example of reading and writing a simple controller change message.

 

Look for a controller 1 change on any MIDI channel from within processBlock:

MidiBuffer::Iterator MidiItr(midiMessages);

MidiMessage MidiMsg;int smpPos;

while(MidiItr.getNextEvent(MidiMsg,smpPos))
{//while midi events found

if(MidiMsg.isController())//check it is a controller change

if(MidiMsg.getControllerNumber()==1)//check it is the one you want

yourParam =((float) MidiMsg.getControllerValue())/127.0f);

}

 

Write a controller change "CC" on MIDI channel "MC" with value 'V" from within processBlock:

midiMessages.addEvent(MidiMessage::controllerEvent(MC,CC,V),0);

 

Alternatively, if you are looking to incorporate changes from outside the normal processing change, JUCE 4.x introduces direct support for OSC.  See the class documentation for OSCReciever and OSCSender.  You use these to manage ports and add listeners which have overrides to process the messages and dispatch any needed parameter changes etc.  


PlayHead and PositionInfo and TempoSync - this can be a good way to dial in your effects to the current bpm or time signature of the host.  From within your processBlock, simply call getPlayHead for access to the AudioPlayHead.  From there, you can get the CurrentPositionInfo which includes tempo, time signatures etc.

 

 

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 for float parameters (an array of AudioParameterFloat* mFloatParam) can take the form below to keep all updates to the parameter list 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]= (*mFloatParam[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++)

*mFloatParam[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!