Oscillator
, EnvelopeGenerator
, MIDIReceiver
and Filter
classes we have already written.Voice
class, representing one sounding note. Then we will create a VoiceManager
class, ensuring that all notes are sounded and drowned out on time.Filter::cutoff
- Filter::cutoff
is called in it. So each voice needs its own filter.Voice
plays its own note, i.e. he has his own frequency, which means his own independent Oscillator
.MIDIReceiver
and one VoiceManager
VoiceManager
has one LFO
and many Voice
Voice
has two Oscillator
, two envelope Generators (for amplitude and filter) and one Filter
Voice
. And, as usual, do not forget to add it to all Xcode targets and all VS projects. In Voice.h add: #include "Oscillator.h" #include "EnvelopeGenerator.h" #include "Filter.h"
private
section: private: Oscillator mOscillatorOne; Oscillator mOscillatorTwo; EnvelopeGenerator mVolumeEnvelope; EnvelopeGenerator mFilterEnvelope; Filter mFilter;
int mNoteNumber; int mVelocity;
double mFilterEnvelopeAmount; double mOscillatorMix; double mFilterLFOAmount; double mOscillatorOnePitchAmount; double mOscillatorTwoPitchAmount; double mLFOValue;
mLFOValue
, are associated with the values of the interface handles. In fact, these values are the same for all voices, but we will not make them global and throw them into the class of the plugin. Each voice needs access to these parameters every sample, and the Voice class does not even know about the existence of a plug-in class ( #include "SpaceBass.h"
). Setting up such access would be time consuming.isMuted
flag to the Oscillator
class? Move it to Voice
so that when the voice is silent, the values of the oscillator, envelopes and filter are not calculated: bool isActive;
public
before private
. Let's start with the constructor: public: Voice() : mNoteNumber(-1), mVelocity(0), mFilterEnvelopeAmount(0.0), mFilterLFOAmount(0.0), mOscillatorOnePitchAmount(0.0), mOscillatorTwoPitchAmount(0.0), mOscillatorMix(0.5), mLFOValue(0.0), isActive(false) { // Set myself free everytime my volume envelope has fully faded out of RELEASE stage: mVolumeEnvelope.finishedEnvelopeCycle.Connect(this, &Voice::setFree); };
Voice
not active. Also, using signals and slots of the EnvelopeGenerator
, we “release” the voice as soon as the amplitude envelope exits the release stage.public
: inline void setFilterEnvelopeAmount(double amount) { mFilterEnvelopeAmount = amount; } inline void setFilterLFOAmount(double amount) { mFilterLFOAmount = amount; } inline void setOscillatorOnePitchAmount(double amount) { mOscillatorOnePitchAmount = amount; } inline void setOscillatorTwoPitchAmount(double amount) { mOscillatorTwoPitchAmount = amount; } inline void setOscillatorMix(double mix) { mOscillatorMix = mix; } inline void setLFOValue(double value) { mLFOValue = value; } inline void setNoteNumber(int noteNumber) { mNoteNumber = noteNumber; double frequency = 440.0 * pow(2.0, (mNoteNumber - 69.0) / 12.0); mOscillatorOne.setFrequency(frequency); mOscillatorTwo.setFrequency(frequency); }
setNoteNumber
. It calculates the frequency for a given note using the formula we already know and passes it to both oscillators. After it, add: double nextSample(); void setFree();
Oscillator::nextSample
gives us an Oscillator
output, so Voice::nextSample
gives the resulting voice value after the amplitude envelope and filter. We write implementation in Voice.cpp : double Voice::nextSample() { if (!isActive) return 0.0; double oscillatorOneOutput = mOscillatorOne.nextSample(); double oscillatorTwoOutput = mOscillatorTwo.nextSample(); double oscillatorSum = ((1 - mOscillatorMix) * oscillatorOneOutput) + (mOscillatorMix * oscillatorTwoOutput); double volumeEnvelopeValue = mVolumeEnvelope.nextSample(); double filterEnvelopeValue = mFilterEnvelope.nextSample(); mFilter.setCutoffMod(filterEnvelopeValue * mFilterEnvelopeAmount + mLFOValue * mFilterLFOAmount); return mFilter.process(oscillatorSum * volumeEnvelopeValue * mVelocity / 127.0); }
nextSample
for both oscillators and mix them according to mOscillatorMix
. When mOscillatorMix
is zero, only oscillatorOneOutput
is heard. At 0.5
both oscillators have equal amplitude.filterEnvelopeValue
to the filter cutoff frequency and take the LFO value into account. The overall modulation of the slice is the sum of the filter envelope and the LFO.mFilter.process
, as a result we get a filtered output, which we return.setFree
extremely simple: void Voice::setFree() { isActive = false; }
mVolumeEnvelope
completely fades out.VoiceManager
. In the header, start with these lines: #include "Voice.h" class VoiceManager { };
private
members of the class: static const int NumberOfVoices = 64; Voice voices[NumberOfVoices]; Oscillator mLFO; Voice* findFreeVoice();
NumberOfVoices
denotes the maximum number of simultaneously sounding voices. The next line creates an array of votes. This structure uses space for 64 voices, so it is better to think about the dynamic allocation of memory . However, the plugin class is new PLUG_CLASS_NAME
dynamically distributed (look for " new PLUG_CLASS_NAME
" in Iplug_include_in_plug_src.h ), so all the members of the plugin class are also on the heap .mLFO
is the global LFO for the plugin. It never restarts, it just oscillates independently. You can bet that it should be inside the plugin class ( VoiceManager
doesn't need to know about the LFO). But this will add another layer of distinction between the voices of the Voice
and the LFO, which means we will need more gluing code .findFreeVoice
is a helper function to search for voices that do not currently sound. Add its implementation to VoiceManager.cpp : Voice* VoiceManager::findFreeVoice() { Voice* freeVoice = NULL; for (int i = 0; i < NumberOfVoices; i++) { if (!voices[i].isActive) { freeVoice = &(voices[i]); break; } } return freeVoice; }
&
reference), because in this case, unlike the reference, you can return NULL
. This will mean that all voices are heard.public
: void onNoteOn(int noteNumber, int velocity); void onNoteOff(int noteNumber, int velocity); double nextSample();
onNoteOn
is called when a MIDI Note On message is received. onNoteOff
, Respectively, is called when the Note Off message. We write the code for these functions in the .cpp class file: void VoiceManager::onNoteOn(int noteNumber, int velocity) { Voice* voice = findFreeVoice(); if (!voice) { return; } voice->reset(); voice->setNoteNumber(noteNumber); voice->mVelocity = velocity; voice->isActive = true; voice->mVolumeEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK); voice->mFilterEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK); }
findFreeVoice
. If nothing is found, we return nothing. This means that when all voices are heard, pressing another key will have no result. The implementation of the voice stealing approach will be one of the topics of the next post. If there is a free voice, we need to update it to the initial state ( reset
, we will do it very soon). After that we set the correct values for setNoteNumber
and mVelocity
. Mark the voice as active and translate both envelopes into the attack stage.private
members of the Voice
from outside. In my opinion, the best solution in this situation would be to use the friend keyword. Add the appropriate line before public
in Voice.h : friend class VoiceManager;
Voice
gives VoiceManager
access to its private
members. I’m not a FooManager
this approach, but if you have the Foo
class and the FooManager
class, this is a good way to avoid writing many setters.onNoteOff
looks like this: void VoiceManager::onNoteOff(int noteNumber, int velocity) { // Find the voice(s) with the given noteNumber: for (int i = 0; i < NumberOfVoices; i++) { Voice& voice = voices[i]; if (voice.isActive && voice.mNoteNumber == noteNumber) { voice.mVolumeEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE); voice.mFilterEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE); } } }
onNoteOff
and translates the envelopes of all five voices into the release stage. Four of them are already in this stage, so let's look at the first line of the EnvelopeGenerator::enterStage
: if (currentStage == newStage) return;
nextSample
member nextSample
for VoiceManager
. It should display the total value for all active votes: double VoiceManager::nextSample() { double output = 0.0; double lfoValue = mLFO.nextSample(); for (int i = 0; i < NumberOfVoices; i++) { Voice& voice = voices[i]; voice.setLFOValue(lfoValue); output += voice.nextSample(); } return output; }
(0.0)
, iterate over all votes, set the current LFO value and add the voice output to the total output. As we remember, if a voice is inactive, its Voice::nextSample
function will not calculate anything and will end immediately.Oscillator
and Filter
objects and used them throughout the plug-in's work. But VoiceManager re-uses free voices, so you have to figure out how to fully transfer the voice to the initial state. Let's start by adding a function to the public
Voice
header: void reset();
void Voice::reset() { mNoteNumber = -1; mVelocity = 0; mOscillatorOne.reset(); mOscillatorTwo.reset(); mVolumeEnvelope.reset(); mFilterEnvelope.reset(); mFilter.reset(); }
mNoteNumber
and mVelocity
are dropped mVelocity
, then oscillators, envelopes and a filter are dropped. Let's write it!public
section of Oscillator.h, add: void reset() { mPhase = 0.0; }
isMuted
flag from the private
section. Remember to remove it also from the constructor initialization list and remove the setMuted
member setMuted
. We now monitor the state of activity at the Voice
level, so the oscillator is no longer necessary. Remove this line from the Oscillator::nextSample
: // remove this line: if(isMuted) return value;
reset
function in the EnvelopeGenerator
slightly longer. In the public
section of the EnvelopeGenerator
write the following: void reset() { currentStage = ENVELOPE_STAGE_OFF; currentLevel = minimumLevel; multiplier = 1.0; currentSampleIndex = 0; nextStageSampleIndex = 0; }
reset
for the Filter
class (also in public
): void reset() { buf0 = buf1 = buf2 = buf3 = 0.0; }
VoiceManager
uses Voice
, it calls the reset
function to reset the voice to its initial state. This function, in turn, resets voice oscillators, its envelope generators and filter.Oscillator
: mOscillatorMode
Filter
: cutoff
, resonance
, mode
EnvelopeGenerator
: stageValue
mOscillatorMode
is static. Then the LFO would have the same waveform as the other oscillators, and we do not want this. Further, if the EnvelopeGenerator
envelope generator stageValue
values were static, the amplitude and filter envelopes would be the same.VolumeEnvelope
and FilterEnvelope
classes that would inherit from the EnvelopeGenerator
class. The stageValue
parameter could be static and VolumeEnvelope
and FilterEnvelope
could change it. This would clearly separate the envelopes and all voices might have access to static members. But in this case we are not talking about large amounts of memory. All that has to do with the structure that we have created is to synchronize these variables between the amplitude envelopes and the filters of all voices.sampleRate
. It makes no sense for the synthesizer components to work at different sampling rates. Let's fix this in Oscillator.h : static double mSampleRate;
mSampleRate(44100.0)
. In Oscillator.cpp, after #include
add: double Oscillator::mSampleRate = 44100.0;
EnvelopeGenerator
. Make sampleRate
static, remove the constructor from the initialization list and add it to EnvelopeGenerator.cpp : double EnvelopeGenerator::sampleRate = 44100.0;
static void setSampleRate(double newSampleRate);
Source: https://habr.com/ru/post/231513/
All Articles