GetDunne Wiki

Notes from the desk of Shane Dunne, software development consultant

User Tools

Site Tools


overview

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

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]
Line 9: Line 9:
 ===== 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 //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>
Line 65: Line 67:
 </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.
  
overview.1504108375.txt.gz · Last modified: 2017/08/30 15:52 by shane