This is an old revision of the document!
In the following, whenever I want to refer to a pair of files, e.g. PluginProcessor.h
/.cpp
, I'll use the typewriter font, but leave off the file extension, e.g. PluginProcessor
.
The VanillaJuce code consists of three groups of files:
PluginProcessor
and PluginEditor
represent the VanillaJuce plugin, as seen from the outside, i.e. by a DAW or other plugin host program.Synth
represent the synthesizer (DSP) aspect.Gui
represent the GUI aspect.
The PluginProcessor
files are the most important. These define a new C++ class VanillaJuceAudioProcessor, derived from the juce::AudioProcessor. Every plugin needs to a juce::AudioProcessor-derived object (this object instance is the plugin). The GUI, which is defined by the PluginEditor
files, is actually optional; the fact that VanillaJuceAudioProcessor::hasEditor() returns true is what tells the plugin host that this particular plugin also has a custom GUI.
The processor needs to be able to notify the GUI editor when it changes one or more synth parameters (e.g. when a new preset is selected), so it can update the GUI display. This can be done in any number of ways, but I chose to have the VanillaJuceAudioProcessor class also derive from the juce::ChangeBroadcaster, and the VanillaJuceAudioProcessorEditor inherit from juce::ChangeListener. The processor calls its sendChangeMessage() function to notify the editor, which results in a call to the editor's changeListenerCallback() function.
The DSP aspect of VanillaJuce is represented by four main classes as follows:
struct
full of member variables representing, e.g., oscillator waveforms, ADSR settings, etc.—all the details which collectively define one synth preset (or “program” in plugin parlance)The JUCE documentation says very little about the SynthesiserSound class. The class itself is almost trivial:
class JUCE_API SynthesiserSound : public ReferenceCountedObject { protected: //============================================================================== SynthesiserSound(); public: /** Destructor. */ virtual ~SynthesiserSound(); //============================================================================== /** Returns true if this sound should be played when a given midi note is pressed. The Synthesiser will use this information when deciding which sounds to trigger for a given note. */ virtual bool appliesToNote (int midiNoteNumber) = 0; /** Returns true if the sound should be triggered by midi events on a given channel. The Synthesiser will use this information when deciding which sounds to trigger for a given note. */ virtual bool appliesToChannel (int midiChannel) = 0; /** The class is reference-counted, so this is a handy pointer class for it. */ typedef ReferenceCountedObjectPtr<SynthesiserSound> Ptr; private: //============================================================================== JUCE_LEAK_DETECTOR (SynthesiserSound) };
The constructor and destructor are empty, and the two pure-virtual member functions appliesToNote() and appliesToChannel() are very simple. appliesToNote() is clearly there to support things like keyboard splits, where different sounds are used for different note ranges, and appliesToChannel() would appear to work similarly to support multi-timbral synths, where different MIDI channels trigger different sounds. But what is this mysterious “sound” thing, and why does this class even exist?
The answer can be found in class juce::SynthesiserVoice, specifically SynthesiserVoice::startNote(). Have a look at this collection of override functions in class SynthVoice. (The ellipses … indicate where other code has been omitted for clarity.)
class SynthVoice : public SynthesiserVoice { ... bool canPlaySound(SynthesiserSound* sound) override { return dynamic_cast<SynthSound*> (sound) != nullptr; } ... void startNote(int midiNoteNumber, float velocity, SynthesiserSound* sound, int currentPitchWheelPosition) override; void stopNote(float velocity, bool allowTailOff) override; void pitchWheelMoved(int newValue) override; void controllerMoved(int controllerNumber, int newValue) override; void renderNextBlock(AudioSampleBuffer& outputBuffer, int startSample, int numSamples) override; ... };
Just by looking at this, you don't have to delve into the source for class juce::Synthesiser to see that its voice-assigning code most likely calls canPlaySound() to ensure that a given voice can actually play the given sound, and if so, calls startNote() with the current MIDI note number, key-down velocity and pitch-wheel position, plus a pointer to the sound object. Hence, unless we choose to add a lot of extra member variables to our SynthVoice class, the only way our voice objects know what sound to make will be via the SynthesiserSound* sound parameter to startNote().
So, here is the VanillaJuce SynthSound class declaration: <code cpp> class SynthSound : public SynthesiserSound { private:
Synth& synth;
public:
SynthSound(Synth& ownerSynth); // our sound applies to all notes, all channels bool appliesToNote(int /*midiNoteNumber*/) override { return true; } bool appliesToChannel(int /*midiChannel*/) override { return true; }
// pointer to currently-used parameters bundle SynthParameters* pParams;
// call to notify owner Synth, that parameters have changed void parameterChanged();
};