Introduction
In this tutorial I will show you how to make JR-Granular , a real-time granular fx plugin, with
RNBO C++ export and JUCE. In particular, I will focus on the following:
how to do the RNBO C++ export
how to connect the exported code and JUCE using APVTS
how to make custom UIs with JUCE
Let’s get started.
Prerequisites
Install Xcode Command Line Tools by running “xcode-select —install”.
Download and install CMake by running “brew install cmake”.
Get Projucer by downloading JUCE .
Making rnbo-granular.maxpat
In this chapter we will quickly implement a real-time granular fx using the gen~ codebox.
Create a rnbo~ object.
Create a gen~ object in the rnbo patcher
Create a codebox object in the gen patcher
Copy the following gen code and paste it into the codebox.
codebox Param interval ( 100 );
Param grainSize ( 100 );
Param grainPos ( 100 );
Param width ( 0.5 );
Param pitch ( 0 );
Param mix ( 0.5 );
Param gain ( 0 );
Delay del_inL ( samplerate * 2 );
Delay del_inR ( samplerate * 2 );
Data data_param ( 6 , 100 );
History his_index ( 0 );
History his_mix ( 0 ), his_gain ( 0 );
his_mix = (mix - his_mix) * 0.001 + his_mix;
his_gain = (gain - his_gain) * 0.001 + his_gain;
maxVoice = 100 ;
del_inL. write (in1);
del_inR. write (in2);
mCout = counter ( 1 , 0 , mstosamps (interval));
if (mCout == 1 ) {
if ( peek (data_param, 0 , his_index) == 0 ) {
poke (data_param, 1 , 0 , his_index);
poke (data_param, mstosamps (grainSize), 1 , his_index);
sizeScaled = mstosamps (grainSize);
widthRand = noise () * 0.5 * width;
poke (data_param, 0.5 + widthRand, 3 , his_index);
poke (data_param, 0.5 - widthRand, 4 , his_index);
pitchScaled = pow ( 2 , pitch / 12 ) - 1 ;
poke (data_param, pitchScaled, 5 , his_index);
his_index = wrap (his_index + 1 , 0 , maxVoice);
posScaled = mstosamps (grainPos) * abs ( noise ());
poke (data_param, posScaled + max (sizeScaled * pitchScaled, 0 ), 2 , his_index);
}
}
outL = 0 ;
outR = 0 ;
totalWin = 0 ;
for (i = 0 ; i < maxVoice; i += 1 ) {
pCout = peek (data_param, 0 , i);
if (pCout != 0 ) {
s = peek (data_param, 1 , i);
if (pCout < s) {
pCoutScaled = pCout / s;
win = 0.5 * ( sin ( 1.5 * PI + pCoutScaled * TWOPI) + 1 );
outputL = del_inL. read ( peek (data_param, 2 , i) - pCout * peek (data_param, 5 , i)) * win;
outputR = del_inR. read ( peek (data_param, 2 , i) - pCout * peek (data_param, 5 , i)) * win;
outL += outputL * peek (data_param, 3 , i);
outR += outputR * peek (data_param, 4 , i);
poke (data_param, pCout + 1 , 0 , i);
totalWin += win;
} else {
poke (data_param, 0 , 0 , i);
}
}
}
normalise = pow ( 1 / max (totalWin, 1 ), 0.3 ) * his_gain;
out1 = mix (in1, outL * normalise, his_mix);
out2 = mix (in2, outR * normalise, his_mix);
Finally, create param and setparam objects as shown below:
Setting up a JUCE project
First, open Projucer and create a new project named JRGranular to generate the template files PluginProcessor.h/cpp and PluginEditor.h/cpp.
We will build our projects using CMake, so we will not use Projucer anymore.
Go to the project directory and delete unnecessary files.
$ cd ~/Desktop/JRGranular
$ rm -rf Builds JuceLibraryCode JRGranular.jucer
Add JUCE v7.0.2 to the project with the following command:
$ git init
$ git submodule add https://github.com/juce-framework/JUCE.git External/JUCE
$ cd External/JUCE
$ git checkout 7.0.2
$ cd -
Create RnboExport directory for RNBO C++ export destination.
$ mkdir RnboExport
Create a CMakeLists.txt file, copy and paste the following code into it
$ touch CMakeLists.txt
CMakeLists.txt cmake_minimum_required ( VERSION 3.21)
set ( PROJECT_NAME "JR-Granular" )
set (CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Build architectures for macOS" )
set (CMAKE_OSX_DEPLOYMENT_TARGET "10.11" CACHE STRING "Minimum macOS version required" )
project ( "${PROJECT_NAME}" VERSION 1.0.0)
set (formats "VST3" "Standalone" )
if ( APPLE )
list (APPEND formats "AU" )
endif ()
add_subdirectory (External/JUCE)
juce_add_plugin( "${PROJECT_NAME}"
VERSION "1.0.0"
COMPANY_NAME "JR-Granular"
PLUGIN_MANUFACTURER_CODE "Jrgn"
PLUGIN_CODE "R001"
FORMATS ${formats}
COPY_PLUGIN_AFTER_BUILD TRUE
IS_SYNTH FALSE
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
IS_MIDI_EFFECT FALSE
PRODUCT_NAME "${PROJECT_NAME}" )
juce_generate_juce_header( "${PROJECT_NAME}" )
include_directories (
"${CMAKE_CURRENT_SOURCE_DIR}/RnboExport/rnbo"
"${CMAKE_CURRENT_SOURCE_DIR}/RnboExport/rnbo/common" )
target_sources ( "${PROJECT_NAME}"
PRIVATE
RnboExport/rnbo_granular.cpp
RnboExport/rnbo/RNBO.cpp
Source/PluginProcessor.cpp
Source/PluginEditor.cpp)
target_compile_features ( "${PROJECT_NAME}" PUBLIC cxx_std_20)
target_compile_definitions ( "${PROJECT_NAME}"
PUBLIC
JUCE_WEB_BROWSER=0
JUCE_USE_CURL=0
JUCE_VST3_CAN_REPLACE_VST2=0)
target_link_libraries ( "${PROJECT_NAME}"
PRIVATE
juce::juce_audio_utils
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags)
After these settings, your directory structure should look something like this:
RNBO C++ Export
Click on the export sidebar button located in the right toolbar, and select the C++ Source Code Export.
Click on the choose button to specify the RnboExport directory as the output directory.
Change the Export Name and the Classname as follows:
Click on the Export to Selected Target button at the bottom right of the window. If there are no error messages in the Export Log, you have succeeded.
Return to the console and check that the project can be built successfully.
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
$ cmake --build build --config Debug
Run the standalone with the following command. You should see “Hello, World”.
$ build/JR-Granular_artefacts/Debug/Standalone/JR-Granular.app/Contents/MacOS/JR-Granular
Connecting the exported code with JUCE
This chapter explains how to link the exported c++ code with JUCE. My approach is to use juce::AudioProcessorValueTreeState .
ParamIDs.h
First, create the string IDs in ParamIDs.h, which are needed when creating JUCE apvts parameters and when linking the UI and DSP. I do this to prevent typing errors. The value of each variable must correspond to the value of “paramId” listed in the RnboExport/description.json.
$ touch Source/ParamIDs.h
ParamIDs.h #pragma once
namespace ParamIDs
{
// Be the same value as the "paramId" listed in the rnbo description.json.
inline constexpr auto mix { "mix" };
inline constexpr auto gain { "gain" };
inline constexpr auto grainPos { "grainPos" };
inline constexpr auto grainSize { "grainSize" };
inline constexpr auto interval { "interval" };
inline constexpr auto pitch { "pitch" };
inline constexpr auto width { "width" };
} // namespace paramIDs
PluginProcessor.h/cpp
PluginProcessor.h/cpp is as follows.
PluginProcessor.h #pragma once
#include <JuceHeader.h>
#include "RNBO.h"
class JRGranularAudioProcessor : public juce :: AudioProcessor ,
public juce :: AudioProcessorValueTreeState :: Listener
{
public:
//==============================================================================
JRGranularAudioProcessor ();
~JRGranularAudioProcessor () override ;
//==============================================================================
void prepareToPlay ( double sampleRate , int samplesPerBlock ) override ;
void releaseResources () override ;
#ifndef JucePlugin_PreferredChannelConfigurations
bool isBusesLayoutSupported ( const BusesLayout & layouts ) const override ;
#endif
void processBlock ( juce :: AudioBuffer < float > & , juce :: MidiBuffer & ) override ;
//==============================================================================
juce :: AudioProcessorEditor * createEditor () override ;
bool hasEditor () const override ;
//==============================================================================
const juce :: String getName () const override ;
bool acceptsMidi () const override ;
bool producesMidi () const override ;
bool isMidiEffect () const override ;
double getTailLengthSeconds () const override ;
//==============================================================================
int getNumPrograms () override ;
int getCurrentProgram () override ;
void setCurrentProgram ( int index ) override ;
const juce :: String getProgramName ( int index ) override ;
void changeProgramName ( int index , const juce :: String & newName ) override ;
//==============================================================================
void getStateInformation ( juce :: MemoryBlock & destData ) override ;
void setStateInformation ( const void* data , int sizeInBytes ) override ;
void parameterChanged ( const juce :: String & parameterID , float newValue ) override ;
private:
//==============================================================================
juce ::AudioProcessorValueTreeState apvts;
juce ::UndoManager undoManager;
RNBO ::CoreObject rnboObject;
std ::unordered_map < juce ::String, RNBO ::ParameterIndex > apvtsParamIdToRnboParamIndex;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JRGranularAudioProcessor)
};
PluginProcessor.cpp #include "PluginProcessor.h"
#include "PluginEditor.h"
#include "ParamIDs.h"
static juce :: AudioProcessorValueTreeState :: ParameterLayout createParameterLayout ()
{
juce :: AudioProcessorValueTreeState ::ParameterLayout layout;
auto msFormat = []( float value , int )
{
if (value < 100.0 f )
return juce :: String (value, 1 ) + " ms" ;
else
return juce :: String ( std :: roundf (value)) + " ms" ;
};
layout. add ( std :: make_unique < juce :: AudioParameterFloat > ( juce ::ParameterID { ParamIDs ::interval, 1 },
"intv" ,
juce :: NormalisableRange < float > ( 10.0 f , 500.0 f , 0.01 f , 0.405 f ),
100.0 f ,
juce :: String (),
juce :: AudioProcessorParameter ::genericParameter,
msFormat,
nullptr ));
layout. add ( std :: make_unique < juce :: AudioParameterFloat > ( juce ::ParameterID { ParamIDs ::pitch, 1 },
ParamIDs ::pitch,
juce :: NormalisableRange < float > ( - 12.0 f , 12.0 f , 0.1 f , 1.0 f ),
0.0 f ,
juce :: String (),
juce :: AudioProcessorParameter ::genericParameter,
[]( float value , int ) {
return juce :: String (value, 1 ) + " st" ; },
nullptr ));
layout. add ( std :: make_unique < juce :: AudioParameterFloat > ( juce ::ParameterID { ParamIDs ::grainPos, 1 },
"pos" ,
juce :: NormalisableRange < float > ( 10.0 f , 500.0 f , 1.0 f , 0.405 f ),
100.0 f ,
juce :: String (),
juce :: AudioProcessorParameter ::genericParameter,
msFormat,
nullptr ));
layout. add ( std :: make_unique < juce :: AudioParameterFloat > ( juce ::ParameterID { ParamIDs ::grainSize, 1 },
"size" ,
juce :: NormalisableRange < float > ( 10.0 f , 500.0 f , 0.01 f , 0.405 f ),
100.0 f ,
juce :: String (),
juce :: AudioProcessorParameter ::genericParameter,
msFormat,
nullptr ));
auto percentFormat = []( float value , int )
{
if (value < 10.0 f )
return juce :: String (value, 2 ) + " %" ;
else if (value < 100.0 f )
return juce :: String (value, 1 ) + " %" ;
else
return juce :: String (value, 0 ) + " %" ;
};
layout. add ( std :: make_unique < juce :: AudioParameterFloat > ( juce ::ParameterID { ParamIDs ::mix, 1 },
ParamIDs ::mix,
juce :: NormalisableRange < float > ( 0.0 f , 100.0 f , 0.01 f , 1.0 f ),
50.0 f ,
juce :: String (),
juce :: AudioProcessorParameter ::genericParameter,
percentFormat,
nullptr ));
layout. add ( std :: make_unique < juce :: AudioParameterFloat > ( juce ::ParameterID { ParamIDs ::width, 1 },
ParamIDs ::width,
juce :: NormalisableRange < float > ( 0.0 f , 100.0 f , 0.01 f , 1.0 f ),
50.0 ,
juce :: String (),
juce :: AudioProcessorParameter ::genericParameter,
percentFormat,
nullptr ));
layout. add ( std :: make_unique < juce :: AudioParameterFloat > ( juce ::ParameterID { ParamIDs ::gain, 1 },
"vol" ,
juce :: NormalisableRange < float > ( - 36.0 f , 12.0 f , 0.1 f , 2.4 f ),
0.0 f ,
juce :: String (),
juce :: AudioProcessorParameter ::genericParameter,
[]( float value , int ) {
if ( - 10.0 f < value && value < 10.0 f )
return juce :: String (value, 1 ) + " dB" ;
else
return juce :: String ( std :: roundf (value), 0 ) + " dB" ; },
nullptr ));
return layout;
}
//==============================================================================
JRGranularAudioProcessor :: JRGranularAudioProcessor ()
#ifndef JucePlugin_PreferredChannelConfigurations
: AudioProcessor ( BusesProperties ()
#if ! JucePlugin_IsMidiEffect
#if ! JucePlugin_IsSynth
. withInput ( "Input" , juce :: AudioChannelSet :: stereo (), true )
#endif
. withOutput ( "Output" , juce :: AudioChannelSet :: stereo (), true )
#endif
),
#endif
apvts ( * this , & undoManager, "Parameters" , createParameterLayout ())
{
for ( RNBO ::ParameterIndex i = 0 ; i < rnboObject. getNumParameters (); ++ i)
{
RNBO ::ParameterInfo info;
rnboObject. getParameterInfo (i, & info);
if (info.visible)
{
auto paramID = juce :: String (rnboObject. getParameterId (i));
// Each apvts parameter id and range must be the same as the rnbo param object's.
// If you hit this assertion then you need to fix the incorrect id in ParamIDs.h.
jassert (apvts. getParameter (paramID) != nullptr );
// If you hit these assertions then you need to fix the incorrect apvts
// parameter range in createParameterLayout().
jassert (info.min == apvts. getParameterRange (paramID).start);
jassert (info.max == apvts. getParameterRange (paramID).end);
apvtsParamIdToRnboParamIndex[paramID] = i;
apvts. addParameterListener (paramID, this );
rnboObject. setParameterValue (i, apvts. getRawParameterValue (paramID)-> load ());
}
}
}
JRGranularAudioProcessor :: ~ JRGranularAudioProcessor ()
{
}
//==============================================================================
const juce :: String JRGranularAudioProcessor :: getName () const
{
return JucePlugin_Name;
}
bool JRGranularAudioProcessor :: acceptsMidi () const
{
#if JucePlugin_WantsMidiInput
return true ;
#else
return false ;
#endif
}
bool JRGranularAudioProcessor :: producesMidi () const
{
#if JucePlugin_ProducesMidiOutput
return true ;
#else
return false ;
#endif
}
bool JRGranularAudioProcessor :: isMidiEffect () const
{
#if JucePlugin_IsMidiEffect
return true ;
#else
return false ;
#endif
}
double JRGranularAudioProcessor :: getTailLengthSeconds () const
{
return 0.0 ;
}
int JRGranularAudioProcessor :: getNumPrograms ()
{
return 1 ; // NB: some hosts don't cope very well if you tell them there are 0 programs,
// so this should be at least 1, even if you're not really implementing programs.
}
int JRGranularAudioProcessor :: getCurrentProgram ()
{
return 0 ;
}
void JRGranularAudioProcessor :: setCurrentProgram ( int index )
{
juce :: ignoreUnused (index);
}
const juce :: String JRGranularAudioProcessor :: getProgramName ( int index )
{
juce :: ignoreUnused (index);
return {};
}
void JRGranularAudioProcessor :: changeProgramName ( int index , const juce :: String & newName )
{
juce :: ignoreUnused (index, newName);
}
//==============================================================================
void JRGranularAudioProcessor :: prepareToPlay ( double sampleRate , int samplesPerBlock )
{
rnboObject. prepareToProcess (sampleRate, static_cast<size_t> (samplesPerBlock));
}
void JRGranularAudioProcessor :: releaseResources ()
{
// When playback stops, you can use this as an opportunity to free up any
// spare memory, etc.
}
#ifndef JucePlugin_PreferredChannelConfigurations
bool JRGranularAudioProcessor :: isBusesLayoutSupported ( const BusesLayout & layouts ) const
{
#if JucePlugin_IsMidiEffect
juce :: ignoreUnused (layouts);
return true ;
#else
// This is the place where you check if the layout is supported.
// In this template code we only support mono or stereo.
// Some plugin hosts, such as certain GarageBand versions, will only
// load plugins that support stereo bus layouts.
if (layouts. getMainOutputChannelSet () != juce :: AudioChannelSet :: mono ()
&& layouts. getMainOutputChannelSet () != juce :: AudioChannelSet :: stereo ())
return false ;
// This checks if the input layout matches the output layout
#if ! JucePlugin_IsSynth
if (layouts. getMainOutputChannelSet () != layouts. getMainInputChannelSet ())
return false ;
#endif
return true ;
#endif
}
#endif
void JRGranularAudioProcessor :: processBlock ( juce :: AudioBuffer < float > & buffer , juce :: MidiBuffer & midiMessages )
{
juce :: ignoreUnused (midiMessages);
auto bufferSize = buffer. getNumSamples ();
rnboObject. prepareToProcess ( getSampleRate (),
static_cast<size_t> (bufferSize));
rnboObject. process (buffer. getArrayOfWritePointers (),
static_cast< RNBO ::Index > (buffer. getNumChannels ()),
buffer. getArrayOfWritePointers (),
static_cast< RNBO ::Index > (buffer. getNumChannels ()),
static_cast< RNBO ::Index > (bufferSize));
}
//==============================================================================
bool JRGranularAudioProcessor :: hasEditor () const
{
return true ; // (change this to false if you choose to not supply an editor)
}
juce :: AudioProcessorEditor * JRGranularAudioProcessor :: createEditor ()
{
return new JRGranularAudioProcessorEditor ( * this , apvts, undoManager);
/* return new juce::GenericAudioProcessorEditor (*this); */
}
//==============================================================================
void JRGranularAudioProcessor :: getStateInformation ( juce :: MemoryBlock & destData )
{
juce ::MemoryOutputStream mos (destData, true );
apvts.state. writeToStream (mos);
}
void JRGranularAudioProcessor :: setStateInformation ( const void* data , int sizeInBytes )
{
auto tree = juce :: ValueTree :: readFromData (data, static_cast<size_t> (sizeInBytes));
if (tree. isValid ())
apvts. replaceState (tree);
}
void JRGranularAudioProcessor :: parameterChanged ( const juce :: String & parameterID , float newValue )
{
rnboObject. setParameterValue (apvtsParamIdToRnboParamIndex[parameterID], newValue);
}
//==============================================================================
// This creates new instances of the plugin..
juce ::AudioProcessor * JUCE_CALLTYPE createPluginFilter ()
{
return new JRGranularAudioProcessor ();
}
Now you have completed the minimum configuration required to link the exported code to JUCE.
GenericAudioProcessorEditor
Using juce::GenericAudioProcessorEditor, you can quickly build the plugin without implementing the UI. Edit the following and build it.
PluginProcessor.cpp juce :: AudioProcessorEditor * JRGranularAudioProcessor :: createEditor ()
{
/* return new JRGranularAudioProcessorEditor (*this, apvts, undoManager); */
return new juce :: GenericAudioProcessorEditor ( * this );
}
$ rm -rf build
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
$ cmake --build build --config Release
After the successful build, you can run the VST3/AU plugin on your DAW.
Customising the JUCE UI
Preparing for the customisation
We will use the plugin editor, so disable the GenericAudioProcessorEditor:
PluginProcessor.cpp juce :: AudioProcessorEditor * JRGranularAudioProcessor :: createEditor ()
{
return new JRGranularAudioProcessorEditor ( * this , apvts, undoManager);
/* return new juce::GenericAudioProcessorEditor (*this); */
}
Create customised colours for use in the UI.
$ touch Source/MyColours.h
MyColours.h #pragma once
#include <JuceHeader.h>
namespace MyColours
{
const juce ::Colour blue { 0x ff6dd4ff };
const juce ::Colour cream { 0x fff6f9e4 };
const juce ::Colour grey { 0x ffa0a0a0 };
const juce ::Colour midGrey { 0x ff666666 };
const juce ::Colour blackGrey { 0x ff353535 };
const juce ::Colour black { 0x ff141414 };
const juce ::Colour red { 0x ffff3f3f };
}
After this, your directory structure should look something like this:
Edit PluginEditor.h/cpp as follows. This section implements the Undo/Redo features and other small UI settings.
PluginEditor.h #include <JuceHeader.h>
#include "PluginProcessor.h"
class JRGranularAudioProcessorEditor : public juce :: AudioProcessorEditor
{
public:
JRGranularAudioProcessorEditor (JRGranularAudioProcessor & p,
juce ::AudioProcessorValueTreeState & state,
juce ::UndoManager & um);
~JRGranularAudioProcessorEditor () override ;
void paint ( juce :: Graphics & ) override ;
void resized () override ;
bool keyPressed ( const juce :: KeyPress & key ) override ;
private:
JRGranularAudioProcessor & audioProcessor;
juce ::UndoManager & undoManager;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JRGranularAudioProcessorEditor)
};
PluginEditor.cpp #include "PluginProcessor.h"
#include "PluginEditor.h"
#include "MyColours.h"
JRGranularAudioProcessorEditor :: JRGranularAudioProcessorEditor (JRGranularAudioProcessor & p,
juce ::AudioProcessorValueTreeState & state,
juce ::UndoManager & um)
: AudioProcessorEditor ( & p), audioProcessor (p), undoManager (um)
{
setWantsKeyboardFocus ( true );
setSize ( 440 , 280 );
}
JRGranularAudioProcessorEditor :: ~ JRGranularAudioProcessorEditor ()
{
}
void JRGranularAudioProcessorEditor :: paint ( juce :: Graphics & g )
{
g. fillAll ( MyColours ::black);
}
void JRGranularAudioProcessorEditor :: resized ()
{
}
bool JRGranularAudioProcessorEditor :: keyPressed ( const juce :: KeyPress & key )
{
const auto cmdZ = juce ::KeyPress { 'z' , juce :: ModifierKeys ::commandModifier, 0 };
if (key == cmdZ && undoManager. canUndo ())
{
undoManager. undo ();
return true ;
}
const auto cmdShiftZ = juce ::KeyPress { 'z' , juce :: ModifierKeys ::commandModifier
| juce :: ModifierKeys ::shiftModifier, 0 };
if (key == cmdShiftZ && undoManager. canRedo ())
{
undoManager. redo ();
return true ;
}
return false ;
}
After completing the above settings, build and run the standalone and you should see a black editor as shown below.
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
$ cmake --build build --config Debug
$ build/JR-Granular_artefacts/Debug/Standalone/JR-Granular.app/Contents/MacOS/JR-Granular
Adding the custom dial component
This chapter adds customised UI components.
This Dial has the same look and feel as Ableton’s Dial and reproduces equivalent features:
value changes: dragging or arrow keys
fine mode: shift + dragging or shift + arrow keys
edit mode: 0-9
undo: [mac] cmd + z, [win] ctrl + z
redo: [mac] cmd + shift + z, [win] ctrl + shift + z
reset: double click
Create Dial.h/cpp files.
$ touch Source/Dial.{h,cpp}
Copy and paste the contents of Dial.h/cpp from the link below.
Add the following line to the CMakeLists.txt file.
CMakeLists.txt ...
target_sources("${PROJECT_NAME}"
PRIVATE
RnboExport/rnbo_granular.cpp
RnboExport/rnbo/RNBO.cpp
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
+ Source/Dial.cpp)
...
Include the Dial.h file and declare the Dial objects.
PluginEditor.h #include "Dial.h"
PluginEditor.h class JRGranularAudioProcessorEditor : public juce :: AudioProcessorEditor
{
public:
...
private:
...
Dial intvDial;
Dial pitchDial;
Dial sizeDial;
Dial posDial;
Dial widthDial;
Dial mixDial;
Dial gainDial;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JRGranularAudioProcessorEditor)
};
Connect the corresponding APVTS parameters to each Dial and set them to be displayed in the plugin editor.
PluginEditor.cpp #include "ParamIDs.h"
PluginEditor.cpp JRGranularAudioProcessorEditor :: JRGranularAudioProcessorEditor (JRGranularAudioProcessor & p,
juce ::AudioProcessorValueTreeState & state,
juce ::UndoManager & um)
: AudioProcessorEditor ( & p), audioProcessor (p), undoManager (um),
intvDial ( * state. getParameter ( ParamIDs ::interval), & um),
pitchDial ( * state. getParameter ( ParamIDs ::pitch), & um),
sizeDial ( * state. getParameter ( ParamIDs ::grainSize), & um),
posDial ( * state. getParameter ( ParamIDs ::grainPos), & um),
widthDial ( * state. getParameter ( ParamIDs ::width), & um),
mixDial ( * state. getParameter ( ParamIDs ::mix), & um),
gainDial ( * state. getParameter ( ParamIDs ::gain), & um)
{
setWantsKeyboardFocus ( true );
setSize ( 440 , 280 );
intvDial. setLabelText ( "intv" );
mixDial. setLabelText ( "mix" );
posDial. setLabelText ( "pos" );
sizeDial. setLabelText ( "size" );
gainDial. setLabelText ( "vol" );
// Set interval of values changed by arrow keys or shift + arrow keys.
intvDial. setInterval ( 5.0 f );
intvDial. setFineInterval ( 1.0 f );
sizeDial. setInterval ( 5.0 f );
sizeDial. setFineInterval ( 1.0 f );
posDial. setInterval ( 5.0 f );
posDial. setFineInterval ( 1.0 f );
pitchDial. setInterval ( 1.0 f );
pitchDial. setFineInterval ( 0.1 f );
gainDial. setInterval ( 1.0 f );
gainDial. setFineInterval ( 0.1 f );
addAndMakeVisible (sizeDial);
addAndMakeVisible (posDial);
addAndMakeVisible (intvDial);
addAndMakeVisible (widthDial);
addAndMakeVisible (pitchDial);
addAndMakeVisible (mixDial);
addAndMakeVisible (gainDial);
}
PluginEditor.cpp void JRGranularAudioProcessorEditor :: resized ()
{
intvDial. setBounds ( 30 , 30 , 80 , 95 );
pitchDial. setBounds ( 130 , 30 , 80 , 95 );
sizeDial. setBounds ( 230 , 30 , 80 , 95 );
posDial. setBounds ( 330 , 30 , 80 , 95 );
widthDial. setBounds ( 80 , 155 , 80 , 95 );
mixDial. setBounds ( 180 , 155 , 80 , 95 );
gainDial. setBounds ( 280 , 155 , 80 , 95 );
}
Embedding a custom font
The font embedded in JR-Granular is FuturaMedium.ttf font, which is automatically installed in macOS.
Launch Font Book application and look for Futura Medium Font.
Use Finder application to show where this font is on your system. You can find the .ttc file in this way:
A .ttc file is like a collection of .ttf files, and in the case of Futura.ttc, it contains the following .ttf files:
Futura-Medium.ttf
Futura-MediumItalic.ttf
Futura-Bold.ttf
Futura-CondensedMedium.ttf
Futura-CondensedExtraBold.ttf
Since we only want to use Futura-Medium.ttf, use the tool below to break down the .ttc file into multiple .ttf files and download only the Futura-Medium.ttf.
Create Resources directory, rename the downloaded Futura-Medium-01.ttf to FuturaMedium.ttf and place it under the directory.
$ mkdir Resources
$ mv ~/Downloads/Futura-Medium-01.ttf Resources/FuturaMedium.ttf
After these settings, your directory structure should look something like this:
Go to the CMakeLists.txt, add juce_add_binary_data function and edit target_link_libraries function:
CMakeLists.txt + juce_add_binary_data(BinaryData
+ SOURCES
+ Resources/FuturaMedium.ttf)
target_link_libraries("${PROJECT_NAME}"
PRIVATE
+ BinaryData
juce::juce_audio_utils
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags)
Finally, add the following code in the constructor of the plugin editor.
pluginEditor.cpp JRGranularAudioProcessorEditor :: JRGranularAudioProcessorEditor (JRGranularAudioProcessor & p,
juce ::AudioProcessorValueTreeState & state,
juce ::UndoManager & um)
...
{
auto futuraMedium = juce :: Typeface :: createSystemTypefaceFor ( BinaryData ::FuturaMedium_ttf,
BinaryData ::FuturaMedium_ttfSize);
juce :: LookAndFeel :: getDefaultLookAndFeel (). setDefaultSansSerifTypeface (futuraMedium);
...
}
Building
All done! Build and run it on your DAW.
$ rm -rf build
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
$ cmake --build build --config Release
Closing
In this tutorial I have explained the following:
how to do the RNBO C++ export
how to include the exported code into a JUCE project
how to connect the exported code and JUCE using APVTS
If there are any typos or better ways of the implementation, please do comment. Happy Coding!
References