Next revision
|
Previous revision
|
overview [2017/08/30 15:52] shane created |
overview [2017/08/30 16:43] (current) shane [The SynthSound object and class juce::SynthesiserSound] |
===== The "processor" object ===== | ===== The "processor" object ===== |
| |
The ''PluginProcessor'' files are the most important. These define a new C++ class //VanillaJuceAudioProcessor//, derived from the JUCE superclass //AudioProcessor//. Every plugin needs to an //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 ''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 mix-in class //ChangeBroadcaster//, and the //VanillaJuceAudioProcessorEditor// inherit from //ChangeListener//. The processor calls its //sendChangeMessage()// function to notify the editor, which results in a call to the editor's //changeListenerCallback()// function. | 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 "Synth" objects ===== |
| |
The DSP aspect of VanillaJuce is represented by four main classes as follows: | The DSP aspect of VanillaJuce is represented by four main classes as follows: |
* //Synth// (derived from the JUCE //Synthesiser// class) represents the synthesizer itself | * //Synth// (derived from //juce::Synthesiser//) represents the synthesizer itself |
* There is exactly one //Synth// instance, which is a member variable of //VanillaJuceAudioProcessor//. | * There is exactly one //Synth// instance, which is a member variable of //VanillaJuceAudioProcessor//. |
* //SynthVoice// (derived from //SynthesiserVoice//) represents the whole sound-generating apparatus. | * //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 | * //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. | * 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) | * //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. | * The //VanillaJuceAudioProcessor// object has a //programBank// member variable, which is an array of 128 //SynthParameters// objects. |
* //SynthSound// (derived from the JUCE class //SynthesiserSound//) serves to link the other three classes. | * //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 //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 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 ===== | ===== The SynthSound object and class juce::SynthesiserSound ===== |
The JUCE documentation says very little about the //SynthesiserSound// class. The class itself is almost trivial: | The JUCE documentation says very little about the //SynthesiserSound// class. The class itself is almost trivial: |
<code cpp> | <code cpp> |
</code> | </code> |
| |
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" object//? | 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.) |
| <code cpp> |
| 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; |
| |
| ... |
| }; |
| </code> |
| |
| 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(); |
| }; |
| </code> |
| 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: |
| <code cpp> |
| void SynthSound::parameterChanged() |
| { |
| synth.soundParameterChanged(); |
| } |
| </code> |
| //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.) |
| <code cpp> |
| 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<SynthVoice*>(voices.getUnchecked(i)); |
| if (voice->isVoiceActive()) |
| voice->soundParameterChanged(); |
| } |
| } |
| </code> |
| 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. |
| |