GetDunne Wiki

Notes from the desk of Shane Dunne, software development consultant

User Tools

Site Tools


the_plugin_processor

The VanillaJuce plugin Processor

The class VanillaJuceAudioProcessor is arguably the most complicated of all, because, as an instance of the parent class juce::AudioProcessor it represents the entire plugin. It therefore has to include several groups of nearly-unrelated functions. Here is the class declaration:

class VanillaJuceAudioProcessor
    : public AudioProcessor
    , public ChangeBroadcaster
{
public:
    enum
    {
        maxNumberOfVoices = 16
    };
 
    VanillaJuceAudioProcessor();
    virtual ~VanillaJuceAudioProcessor();
 
    void prepareToPlay (double sampleRate, int samplesPerBlock) override;
    void releaseResources() override;
 
    void processBlock (AudioSampleBuffer&, MidiBuffer&) override;
 
    AudioProcessorEditor* createEditor() override;
    bool hasEditor() const override;
 
    const String getName() const override;
 
    bool acceptsMidi() const override;
    bool producesMidi() const override;
    double getTailLengthSeconds() const override;
 
    int getNumPrograms() override;
    int getCurrentProgram() override;
    void setCurrentProgram (int index) override;
    const String getProgramName (int index) override;
    void changeProgramName (int index, const String& newName) override;
 
    void getStateInformation (MemoryBlock& destData) override;
    void setStateInformation (const void* data, int sizeInBytes) override;
    void getCurrentProgramStateInformation(MemoryBlock& destData) override;
    void setCurrentProgramStateInformation(const void* data, int sizeInBytes) override;
 
public:
    SynthSound* getSound() { return pSound; }
 
private:
    Synth synth;
    SynthSound* pSound;
    SynthParameters programBank[kNumberOfPrograms];
    int currentProgram;
 
private:
    void initializePrograms();
 
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VanillaJuceAudioProcessor)
};

Many of the member functions are trivial, so I won't cover them in any detail here.

Constructor and Destructor

The constructor instantiates all other required objects and connects them as required:

VanillaJuceAudioProcessor::VanillaJuceAudioProcessor()
    : currentProgram(0)
{
    initializePrograms();
 
    for (int i = 0; i < maxNumberOfVoices; ++i)
        synth.addVoice(new SynthVoice());
 
    pSound = new SynthSound(synth);
    pSound->pParams = &programBank[currentProgram];
    synth.addSound(pSound);
}

The destructor is actually empty. It's not necessary to call delete on the created SynthVoice and SynthSound objects, because the synth object takes ownership of them via the add…() calls, and will delete them itself when its destructor gets called.

setCurrentProgram()

The setCurrentProgram() function gets called whenever the user changes the current preset through the plugin host:

void VanillaJuceAudioProcessor::setCurrentProgram (int index)
{
    currentProgram = index;
    pSound->pParams = &programBank[currentProgram];
    sendChangeMessage();
}

This code changes the pParams pointer inside the shared SynthSound object, which propagates the change to subsequently-played notes (we don't attempt to change currently-sounding notes in response to program changes), then calls sendChangeMessage() to notify the GUI editor, so it can update its display accordingly.

changeProgramName()

The changeProgramName() function gets called whenever the user edits the name of the current preset through the plugin host:

void VanillaJuceAudioProcessor::changeProgramName (int index, const String& newName)
{
    newName.copyToUTF8(programBank[index].programName, kMaxProgramNameLength);
    sendChangeMessage();
}

The present VanillaJuce GUI does not display program names, but I've included a call to sendChangeMessage() to notify the GUI anyway, so I won't forget it. If and when I do add a program-name display to the GUI, it will get updated whenever the name is changed through the plugin host.

The preset-handling functions

The plugin host can call getCurrentProgramStateInformation() to get the current preset as a “blob” of binary data (which it can e.g. save to a .fxp file), or call setCurrentProgramStateInformation() to do the reverse operation. In an early version of VanillaJuce, I simply did a binary copy of the SynthParameters struct, but I realized that would cause problems as I changed the structure over time, so the new code uses JUCE's XML capabilities to save and restore the data in XML format. (I based my code on that in Obxd.)

void VanillaJuceAudioProcessor::getCurrentProgramStateInformation(MemoryBlock& destData)
{
    ScopedPointer<XmlElement> xml = programBank[currentProgram].getXml();
    copyXmlToBinary(*xml, destData);
}
 
void VanillaJuceAudioProcessor::setCurrentProgramStateInformation(const void* data, int sizeInBytes)
{
    ScopedPointer<XmlElement> xml = getXmlFromBinary(data, sizeInBytes);
    programBank[currentProgram].putXml(xml);
    sendChangeMessage();
}

Note that setCurrentProgramStateInformation() also calls sendChangeMessage() because it changes the current preset, and so must notify the GUI.

Most of the details are in the getXml() and putXml() member functions of SynthParameters. The code is unremarkable, but serves to generate and parse XML structures like this (formatted for clarity):

<program
    name="My Patch"
 
    masterLevel="0.1500000000000000222"
    oscBlend="0.16666666666666665741"
    pitchBendUpSemitones="2"
    pitchBendDownSemitones="2"
 
    osc1Waveform="Square"
    osc1PitchOffsetSemitones="0"
    osc1DetuneOffsetCents="-10"
 
    osc2Waveform="Sine"
    osc2PitchOffsetSemitones="0"
    osc2DetuneOffsetCents="10"
 
    ampEgAttackTimeSeconds="1.5040650406504065817"
    ampEgDecayTimeSeconds="0.10000000000000000555"
    ampEgSustainLevel="0.80000000000000004441"
    ampEgReleaseTimeSeconds="0.5"
/>

The getStateInformation() and setStateInformation() do the same thing for an entire bank of patches:

void VanillaJuceAudioProcessor::getStateInformation (MemoryBlock& destData)
{
    XmlElement xml = XmlElement("VanillaJuce");
    xml.setAttribute(String("currentProgram"), currentProgram);
    XmlElement* xprogs = new XmlElement("programs");
    for (int i = 0; i < kNumberOfPrograms; i++)
        xprogs->addChildElement(programBank[i].getXml());
    xml.addChildElement(xprogs);
    copyXmlToBinary(xml, destData);
}

void VanillaJuceAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
    ScopedPointer<XmlElement> xml = getXmlFromBinary(data, sizeInBytes);
    XmlElement* xprogs = xml->getFirstChildElement();
    if (xprogs->hasTagName(String("programs")))
    {
        int i = 0;
        forEachXmlChildElement(*xprogs, xpr)
        {
            programBank[i].setDefaultValues();
            programBank[i].putXml(xpr);
            i++;
        }
    }
    setCurrentProgram(xml->getIntAttribute(String("currentProgram"), 0));
}

Note that setStateInformation() also calls sendChangeMessage() because it changes all presets, including the current one, and so must notify the GUI.

The XML structure for patch banks looks like this, where each <program … /> item is structured as above:

<VanillaJuce currentProgram="0">
  <programs>
    <program name="Default" masterLevel="0.14999999999999999445" ... />
    ... 127 more program objects ...
  </programs>
</VanillaJuce>

Rendering sound: processBlock()

The processBlock() function, which actually renders audio, simply delegates to the Synth object:

void VanillaJuceAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
}
the_plugin_processor.txt · Last modified: 2017/08/30 17:47 by shane