// 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"
#define GUI_HEIGHT 296
#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
EParams
in EParams
Synthesis.cpp :
enum EParams { mWaveform = 0, mAttack, mDecay, mSustain, mRelease, kNumParams };
enum ELayout { kWidth = GUI_WIDTH, kHeight = GUI_HEIGHT, kKeybX = 1, kKeybY = 230 };
OscillatorMode
with a total number of modes:
enum OscillatorMode { OSCILLATOR_MODE_SINE = 0, OSCILLATOR_MODE_SAW, OSCILLATOR_MODE_SQUARE, OSCILLATOR_MODE_TRIANGLE, kNumOscillatorModes };
Oscillator() : mOscillatorMode(OSCILLATOR_MODE_SINE), // ...
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));
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.
ISwitchControl
, pass the coordinates and bind to the mWaveform
parameter.
IKnobMultiControls
.
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.
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; } }
mWaveform
value of type int
converted to the type OscillatorMode
.
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);
// This won't be enough: void EnvelopeGenerator::setStageValue(EnvelopeStage stage, double value) { stageValue[stage] = value; }
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.
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; } } }
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.
currentLevel
.
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); }
nextStageSampleIndex
, nextStageSampleIndex
it does not depend on Sustain .
ProcessDoubleReplacing
:
int velocity = mMIDIReceiver.getLastVelocity(); if (velocity > 0) { mOscillator.setFrequency(mMIDIReceiver.getLastFrequency()); mOscillator.setMuted(false); } else { mOscillator.setMuted(true); }
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; }
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;
public
:
Signal0<> beganEnvelopeCycle; Signal0<> finishedEnvelopeCycle;
enterStage
in EnvelopeGenerator.cpp add:
if (currentStage == newStage) return; if (currentStage == ENVELOPE_STAGE_OFF) { beganEnvelopeCycle(); } if (newStage == ENVELOPE_STAGE_OFF) { finishedEnvelopeCycle(); }
if
for the generator not to loop at the same stage. The meaning of the other two is as follows:
Signal
. Add the following private
functions to Synthesis.h :
inline void onBeganEnvelopeCycle() { mOscillator.setMuted(false); } inline void onFinishedEnvelopeCycle() { mOscillator.setMuted(true); }
mEnvelopeGenerator.beganEnvelopeCycle.Connect(this, &Synthesis::onBeganEnvelopeCycle); mEnvelopeGenerator.finishedEnvelopeCycle.Connect(this, &Synthesis::onFinishedEnvelopeCycle);
Source: https://habr.com/ru/post/227601/