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 VoiceManagerVoiceManager has one LFO and many VoiceVoice has two Oscillator , two envelope Generators (for amplitude and filter) and one FilterVoice . 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 : mOscillatorModeFilter : cutoff , resonance , modeEnvelopeGenerator : stageValuemOscillatorMode 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