====== Overview of the VanillaJuce code ====== 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. - All the files starting with ''Synth'' represent the synthesizer (DSP) aspect. - All the files starting with ''Gui'' represent the GUI aspect. ===== The "processor" object ===== 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. To understand how parameter changes are propagated in the reverse direction---from GUI to synthesizer---we need the following overview some of the objects which make up the DSP aspect of VanillaJuce. ===== The "Synth" objects ===== The DSP aspect of VanillaJuce is represented by four main classes as follows: * //Synth// (derived from //juce::Synthesiser//) represents the synthesizer itself * There is exactly one //Synth// instance, which is a member variable of //VanillaJuceAudioProcessor//. * //SynthVoice// (derived from //juce::SynthesiserVoice//) represents the whole sound-generating apparatus. * //SynthVoice// encapsulates two //SynthOscillator// objects and one //SynthEnvelopeGenerator// object, which it uses to render incoming MIDI to output audio * The //VanillaJuceAudioProcessor// constructor creates 16 //SynthVoice// objects and adds them to the //Synth// instance. * //SynthParameters// (not derived from any JUCE class) is basically a ''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 //VanillaJuceAudioProcessor// object has a //programBank// member variable, which is an array of 128 //SynthParameters// objects. * //SynthSound// (derived from //juce::SynthesiserSound//) serves to link the other three classes. * The //VanillaJuceAudioProcessor// constructor creates exactly one //SynthSound// object and adds it to the //Synth// instance, but retains a pointer to it in its //pSound// member variable. * The //SynthSound// object contains a reference to the //Synth// object (which never changes), and a pointer to the currently-selected preset (a //SynthParameters// object, one of the elements of the processor's //programBank// array) ===== The SynthSound object and class juce::SynthesiserSound ===== 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 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 (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: 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(); }; Member variable //synth// object is a reference to the //Synth// object (which never changes). //pParams// is a pointer to the currently-selected preset. I've made //pParams// public so the //VanillaJuceAudioProcessor// object (which creates and "owns" the one //SynthSound// object) can change it whenever a different preset is selected, so it points to the appropriate entry in the //programBank// array. All the //Gui...// class constructors take a //SynthSound*// argument, so they can use the //pParams// member to access the current parameter values, in order to display and modify them. Furthermore, whenever any part of the GUI changes a parameter value, it calls the //parameterChanged()// function, which is just this: void SynthSound::parameterChanged() { synth.soundParameterChanged(); } //Synth::soundParameterChanged()// simply iterates over all active (currently-sounding) voices, and calls their //soundParameterChanged()// function. (I looked at the code for //juce::Synthesiser// to see how it handles iterating over all voices.) void Synth::soundParameterChanged() { // Some sound parameter has been changed. Notify all active voices. const ScopedLock sl(lock); for (int i = 0; i < voices.size(); ++i) { SynthVoice* const voice = dynamic_cast(voices.getUnchecked(i)); if (voice->isVoiceActive()) voice->soundParameterChanged(); } } The code for //SynthVoice::soundParameterChanged()// is not so trivial, but all it really does is re-initialize the currently sounding note so that the sound changes to reflect whatever was changed in the GUI.