📜 ⬆️ ⬇️

Creating audio plug-ins, part 10

All posts series:
Part 1. Introduction and setup
Part 2. Learning Code
Part 3. VST and AU
Part 4. Digital Distortion
Part 5. Presets and GUI
Part 6. Signal synthesis
Part 7. Receiving MIDI Messages
Part 8. Virtual Keyboard
Part 9. Envelopes
Part 10. Refinement GUI
Part 11. Filter
Part 12. Low-frequency oscillator
Part 13. Redesign
Part 14. Polyphony 1
Part 15. Polyphony 2
Part 16. Antialiasing



Let's add some controls so that you can change the parameters of the envelope and the waveform. Here is the result we want to get ( from here you can download the layered TIFF):



Download and drop the following files into the project:
bg.png
knob.png (file author - Bootsie )
waveform.png
')
As always, append links and IDs in resource.h :

// Unique IDs for each image resource. #define BG_ID 101 #define WHITE_KEY_ID 102 #define BLACK_KEY_ID 103 #define WAVEFORM_ID 104 #define KNOB_ID 105 // Image resource locations for this plug. #define BG_FN "resources/img/bg.png" #define WHITE_KEY_FN "resources/img/whitekey.png" #define BLACK_KEY_FN "resources/img/blackkey.png" #define WAVEFORM_FN "resources/img/waveform.png" #define KNOB_FN "resources/img/knob.png" 


And we change the height of the window to match the size of the background image:

 #define GUI_HEIGHT 296 


Make changes to the Synthesis.rc header :

 #include "resource.h" BG_ID PNG BG_FN WHITE_KEY_ID PNG WHITE_KEY_FN BLACK_KEY_ID PNG BLACK_KEY_FN WAVEFORM_ID PNG WAVEFORM_FN KNOB_ID PNG KNOB_FN 


Now we need to add the parameters for the waveform and the envelope generator stages. EParams in EParams Synthesis.cpp :

 enum EParams { mWaveform = 0, mAttack, mDecay, mSustain, mRelease, kNumParams }; 


The virtual keyboard needs to be moved down:

 enum ELayout { kWidth = GUI_WIDTH, kHeight = GUI_HEIGHT, kKeybX = 1, kKeybY = 230 }; 


In Oscillator.h, you need to add an OscillatorMode with a total number of modes:

 enum OscillatorMode { OSCILLATOR_MODE_SINE = 0, OSCILLATOR_MODE_SAW, OSCILLATOR_MODE_SQUARE, OSCILLATOR_MODE_TRIANGLE, kNumOscillatorModes }; 


In the initialization list, we indicate the sine as the default waveform:

 Oscillator() : mOscillatorMode(OSCILLATOR_MODE_SINE), // ... 


GUI assembly is carried out in the constructor. Add these lines immediately before AttachGraphics(pGraphics) :

 // Waveform switch GetParam(mWaveform)->InitEnum("Waveform", OSCILLATOR_MODE_SINE, kNumOscillatorModes); GetParam(mWaveform)->SetDisplayText(0, "Sine"); // Needed for VST3, thanks plunntic IBitmap waveformBitmap = pGraphics->LoadIBitmap(WAVEFORM_ID, WAVEFORM_FN, 4); pGraphics->AttachControl(new ISwitchControl(this, 24, 53, mWaveform, &waveformBitmap)); // Knob bitmap for ADSR IBitmap knobBitmap = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN, 64); // Attack knob: GetParam(mAttack)->InitDouble("Attack", 0.01, 0.01, 10.0, 0.001); GetParam(mAttack)->SetShape(3); pGraphics->AttachControl(new IKnobMultiControl(this, 95, 34, mAttack, &knobBitmap)); // Decay knob: GetParam(mDecay)->InitDouble("Decay", 0.5, 0.01, 15.0, 0.001); GetParam(mDecay)->SetShape(3); pGraphics->AttachControl(new IKnobMultiControl(this, 177, 34, mDecay, &knobBitmap)); // Sustain knob: GetParam(mSustain)->InitDouble("Sustain", 0.1, 0.001, 1.0, 0.001); GetParam(mSustain)->SetShape(2); pGraphics->AttachControl(new IKnobMultiControl(this, 259, 34, mSustain, &knobBitmap)); // Release knob: GetParam(mRelease)->InitDouble("Release", 1.0, 0.001, 15.0, 0.001); GetParam(mRelease)->SetShape(3); pGraphics->AttachControl(new IKnobMultiControl(this, 341, 34, mRelease, &knobBitmap)); 


First we create an Enum mWaveform parameter. By default, its value is OSCILLATOR_MODE_SINE , and it can have total kNumOscillatorModes values. Then, we load waveform.png . Here 4 denotes the number of frames, as we know. One could use kNumOscillatorModes , which is now also equal to four. But if we add new waveforms and do not change the waveform.png , then everything will crawl. However, this could serve as a reminder that you need to update the image.
Then we create the ISwitchControl , pass the coordinates and bind to the mWaveform parameter.
We load one knob.png file and use it for all four IKnobMultiControls .
We set SetShape so that the handles are more sensitive at small values ​​and coarser at large values. The default values ​​are the same as in the EnvelopeGenerator constructor. But you can choose any other minimum and maximum values.

Handling value changes



As you remember, the reaction to user change of parameters is prescribed in the OnParamChange function in the main .cpp project file:

 void Synthesis::OnParamChange(int paramIdx) { IMutexLock lock(this); switch(paramIdx) { case mWaveform: mOscillator.setMode(static_cast<OscillatorMode>(GetParam(mWaveform)->Int())); break; case mAttack: case mDecay: case mSustain: case mRelease: mEnvelopeGenerator.setStageValue(static_cast<EnvelopeGenerator::EnvelopeStage>(paramIdx), GetParam(paramIdx)->Value()); break; } } 


When mWaveform value of type int converted to the type OscillatorMode .
As you can see, all parameters of the envelope have one line. If we compare EParams and EnvelopeStage enums , we see that both there and there, the Attack, Decay, Sustain and Release stages correspond to the values 1 , 2 , 3 and 4 . Therefore, static_cast<EnvelopeGenerator::EnvelopeStage>(paramIdx) gives the variable envelope stage EnvelopeStage , and GetParam(paramIdx)->Value() gives the value of variable stage. Therefore, we can simply call setStageValue with these two arguments. Only this function has not been written yet. Add the EnvelopeGenerator to the public class:

 void setStageValue(EnvelopeStage stage, double value); 


Imagine for a moment that this function would be a simple setter:

 // This won't be enough: void EnvelopeGenerator::setStageValue(EnvelopeStage stage, double value) { stageValue[stage] = value; } 


What if you change the stageValue[ENVELOPE_STAGE_ATTACK] at the attack stage? Such an implementation does not cause calculateMultiplier and does not recalculate nextStageSampleIndex . The generator will use the new values ​​only the next time it finds itself at this stage. The same with SUSTAIN: I would like to be able to hold a note and simultaneously look for the right level.
Such implementation is inconvenient, and such a plugin would look absolutely unprofessional.
The generator should immediately update the parameters of the current stage when the corresponding knob is spinning. So, you need to call calculateMultiplier with a new time argument and calculate the new value of nextStageSampleIndex :

 void EnvelopeGenerator::setStageValue(EnvelopeStage stage, double value) { stageValue[stage] = value; if (stage == currentStage) { // Re-calculate the multiplier and nextStageSampleIndex if(currentStage == ENVELOPE_STAGE_ATTACK || currentStage == ENVELOPE_STAGE_DECAY || currentStage == ENVELOPE_STAGE_RELEASE) { double nextLevelValue; switch (currentStage) { case ENVELOPE_STAGE_ATTACK: nextLevelValue = 1.0; break; case ENVELOPE_STAGE_DECAY: nextLevelValue = fmax(stageValue[ENVELOPE_STAGE_SUSTAIN], minimumLevel); break; case ENVELOPE_STAGE_RELEASE: nextLevelValue = minimumLevel; break; default: break; } // How far the generator is into the current stage: double currentStageProcess = (currentSampleIndex + 0.0) / nextStageSampleIndex; // How much of the current stage is left: double remainingStageProcess = 1.0 - currentStageProcess; unsigned long long samplesUntilNextStage = remainingStageProcess * value * sampleRate; nextStageSampleIndex = currentSampleIndex + samplesUntilNextStage; calculateMultiplier(currentLevel, nextLevelValue, samplesUntilNextStage); } else if(currentStage == ENVELOPE_STAGE_SUSTAIN) { currentLevel = value; } } } 


The nested if checks if the generator is at the stage limited by the nextStageSampleIndex parameter (ATTACK, DECAY or RELEASE). nextLevelValue is the signal level in the next stage that the envelope is aiming for. Its value is set in the same way as in the enterStage function. The most interesting thing after the switch : at any current stage the generator should work in accordance with the new values ​​for the rest of this stage. For this, the current stage is divided into the past and the remaining parts. First, it is calculated how far in time the generator is already inside the stage. For example, 0.1 means that 10% is passed. RemainingStageProcess reflects, accordingly, how much is left. Now you need to calculate samplesUntilNextStage and update nextStageSampleIndex . And the most important thing is to call calculateMultiplier to go from the currentLevel level to the nextLevelValue for the samplesUntilNextStage samples.
C SUSTAIN is simple: update currentLevel .

Such implementation covers almost all possible cases. It remains to figure out when the generator is in DECAY, and the value of SUSTAIN changes. Now it’s done in such a way that the level drops to the old value, and when the recession stage ends, the level will jump to the new one. To avoid this, add setStageValue to the end:

 if (currentStage == ENVELOPE_STAGE_DECAY && stage == ENVELOPE_STAGE_SUSTAIN) { // We have to decay to a different sustain value than before. // Re-calculate multiplier: unsigned long long samplesUntilNextStage = nextStageSampleIndex - currentSampleIndex; calculateMultiplier(currentLevel, fmax(stageValue[ENVELOPE_STAGE_SUSTAIN], minimumLevel), samplesUntilNextStage); } 


Now there will be a smooth transition to the new level. Here we do not change the nextStageSampleIndex , nextStageSampleIndex it does not depend on Sustain .
Launch the plugin, click on the waveforms and twist the knobs - all changes should immediately be reflected in the sound.

Performance improvement



Take a look at this part of ProcessDoubleReplacing :

 int velocity = mMIDIReceiver.getLastVelocity(); if (velocity > 0) { mOscillator.setFrequency(mMIDIReceiver.getLastFrequency()); mOscillator.setMuted(false); } else { mOscillator.setMuted(true); } 


Remember we decided not to reset the mLastVelocity receiver's mLastVelocity ? This means that after the first note, mOscillator will generate a wave even when no note sounds. Modify the for loop as follows:

 for (int i = 0; i < nFrames; ++i) { mMIDIReceiver.advance(); int velocity = mMIDIReceiver.getLastVelocity(); mOscillator.setFrequency(mMIDIReceiver.getLastFrequency()); leftOutput[i] = rightOutput[i] = mOscillator.nextSample() * mEnvelopeGenerator.nextSample() * velocity / 127.0; } 


Logically, the oscillator should generate a wave when mEnvelopeGenerator.currentStage not equal to ENVELOPE_STAGE_OFF . It means that you need to turn off generation somewhere in mEnvelopeGenerator.enterStage . For the reasons that we discussed in the previous post, we will not call anything directly from here, but will use the signals and slots again. Before defining a class in EnvelopeGenerator.h, add a couple of lines:

 #include "GallantSignal.h" using Gallant::Signal0; 


Then add a couple of signals to the public :

 Signal0<> beganEnvelopeCycle; Signal0<> finishedEnvelopeCycle; 


At the very beginning of enterStage in EnvelopeGenerator.cpp add:

 if (currentStage == newStage) return; if (currentStage == ENVELOPE_STAGE_OFF) { beganEnvelopeCycle(); } if (newStage == ENVELOPE_STAGE_OFF) { finishedEnvelopeCycle(); } 


The first if for the generator not to loop at the same stage. The meaning of the other two is as follows:


Now let's write a reaction to Signal . Add the following private functions to Synthesis.h :

 inline void onBeganEnvelopeCycle() { mOscillator.setMuted(false); } inline void onFinishedEnvelopeCycle() { mOscillator.setMuted(true); } 


When the envelope cycle begins, we let the oscillator generate a wave. When it ends - drown it.
At the end of the constructor in Synthesis.cpp, connect the signals to the slots:

 mEnvelopeGenerator.beganEnvelopeCycle.Connect(this, &Synthesis::onBeganEnvelopeCycle); mEnvelopeGenerator.finishedEnvelopeCycle.Connect(this, &Synthesis::onFinishedEnvelopeCycle); 


That's all! At startup, everything should work. In REAPER, when you click Cmd + Alt + P (on Mac) or Ctrl + Alt + P (on Windows), a performance monitor will appear:



The total track load per processor is highlighted in red. When the note starts to sound, this value should increase, and when it finally subsides, fall, since the oscillator no longer calculates the samples for nothing.

Now we have a completely acceptable envelope generator.
From here you can download the code.

Next time we will create an equally important component of the synthesizer: the filter!

Original article:
martin-finke.de/blog/articles/audio-plugins-012-envelopes-gui

Source: https://habr.com/ru/post/227601/


All Articles