Assignment 1.5: Adding Faust to JUCE

Due: TBD
Deliverable: None

In this assignment we will walk you through adding Faust to your JUCE plugin. This tutorial is based on this tutorial, but adds in parameter control via PGM. Starter code

The key steps to incorporating Faust are:

  1. Use the Faust CLI and a Faust architecture file to build your C++ code
  2. Initialize the Faust DSP and UI classes in your project
  3. Initialize buffers to interface with Faust's compute method
  4. Hook up Faust parameters to your JUCE plugin

1. Faust 2 C++

This part of the tutorial follows along with the Grame tutorial up to "Creating an Empty JUCE Plug-In Project". Let's take a quick detour and look at the demo Faust code for this assignment.

import("stdfaust.lib");

// Mixes between two signals
mixer = (_*wet + _*(1 - wet))/2
with
{
      wet = hslider("dry/wet", 0.5, 0, 1, 0.01);
};
      
// "Ringmod" effect
ringmod = _<: abs(_)*os.osc(freq), _  : mixer
with
{
    freq = hslider("frequency", 1000, 20, 4000, 1);
};

// Main process
process = ringmod;

This code implements a ring modulation effect. There are two parameters controlled by hslider.

Try running this code in the online Faust IDE, and play around with the controls to get a feel for the effect!

Now we will use the Faust CLI to compile ringmod.dsp using the faustMinimal.h architecture file. Our command will be slightly different than in the Grame tutorial:

faust -i -a faustMinimal.h ringmod.dsp -cn RingMod -o  RingMod.h
We have added the additional flag -cn which renames our function class from the default mydsp to RingMod. You should now have a RingMod.h file containing your compiled Faust code.

2. JUCE-ing Faust

Now open a blank JUCE+PGM project and implement the parameters. The code from assignment 0.5 is an ideal candidate. Import the compiled header RingMod.h file into your Projucer project.

Now at the top of your PluginProcessor.h file import your Faust header file.

#include "RingMod.h"
Next, add our Faust classes as member variables in our Plugin class,
std::unique_ptr<::RingMod> fRM;
std::unique_ptr<::MapUI> fUI;
by instantiating unique pointers to our imported ::RingMod and ::MapUI classes. The RingMod class is the compiled class whose name we set in our terminal command. The MapUI class is one of Faust's parameter handling classes. This code does a couple of things.
  1. It utilizes smart pointers to maintain our references to our Faust classes. We no longer have to worry about deleting our pointers in our destructor
  2. We explicity use :: prefix ensure that our Faust classes are in the global namespace to avoid potential namespace collisions with JUCE.

In our .cpp file, we need to initialize our pointers and connect MapUI to RingMod

fRM = std::make_unique<::RingMod>();
fUI = std::make_unique<::MapUI>();
// Attach UI to our DSP
fRM->buildUserInterface(fUI.get());

Since we utilize smart pointers, there's no need to worry about deleting our dynamically allocated classes!

As a last bit of setup, initialize your ring modulator with your plugin's sampling rate in prepareToPlay.

fRM->init(sampleRate);
You're almost there, now we need to get Faust to run!

3. Compute!

The main function used to process our audio in Faust is the compute function, and will normally appear at the end of your header file. Taking a look we have

virtual void compute(int count, FAUSTFLOAT** RESTRICT inputs, FAUSTFLOAT** RESTRICT outputs)
The compute function takes the following arguments: The dimension of our input/output channels is determined by our Faust code. As a fun exercise, take a look at your compiled RingMod.h header and see if you can understand the code in the compute function!

From the compute function, we see we need a 2D float pointer to interface with the compute function. In your class, add a private 2D float pointer member variable:

float ** faustIO;
We only need one pointer because in JUCE we read and write to the same buffer (at least in this case).

In processBlock you will need to grab the size of the incoming buffer, and store its address to faustIO:

int numSamples = buffer.getNumSamples();
faustIO = buffer.getArrayOfWritePointers();
Pretty easy! Thanks JUCE!

Now call compute to process our audio:

fRM->compute(numSamples, faustIO, faustIO);
Also easy! Thanks Faust!

If you compile your project, you should have a working ring modulator effect.

4. Faust Parameters

Add parameters to your layout and connect parameter listeners like you did in Assignment 1. You can reference either your Faust code or the compiled header to correctly set the min, max, step, and default value in juce::NormalisableRange. Note the parameter order is different in JUCE and Faust

Verify that the parameters appear in the PGM editor before moving on.

Now that we have parameter listeners, in the parameterChanged function, we just need to pass our newValue to our MapUI class.

parameterChanged(const juce::String &parameterID, float newValue)
Say we have a frequency JUCE parameter with parameterID freqID. To update the parameter in Faust, check that parameterID matches freqID and use fUI to set the parameter
if (parameterID == freqID)
  fUI->setParameter("frequency", newValue);
The string "frequency" is defined by the name we gave our hslider in our Faust code. Hook up your last parameter and now you should be controlling your Faust code in JUCE.