
// 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/
All Articles