ProcessMidiMsg
function is called in the plugin. In addition to the note in MIDI messages, information about portamento (Pitch Bend) and control commands ( Control Changes , abbreviated CC) can be transmitted, which can be used to automate plug-in parameters. The ProcessMidiMsg
function is passed an IMidiMsg
message that describes the MIDI event in its format-independent form. This description contains the parameters NoteNumber
and Velocity
, which contain information about the pitch of the sound of our oscillator.ProcessDoubleReplacing
function. You also need to remember the time of receipt of the message, so we will leave this information intact for the next filling of the buffer.#define
and #endif
: #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wextra-tokens" #include "IPlug_include_in_plug_hdr.h" #pragma clang diagnostic pop #include "IMidiQueue.h" class MIDIReceiver { private: IMidiQueue mMidiQueue; static const int keyCount = 128; int mNumKeys; // how many keys are being played at the moment (via midi) bool mKeyStatus[keyCount]; // array of on/off for each key (index is note number) int mLastNoteNumber; double mLastFrequency; int mLastVelocity; int mOffset; inline double noteNumberToFrequency(int noteNumber) { return 440.0 * pow(2.0, (noteNumber - 69.0) / 12.0); } public: MIDIReceiver() : mNumKeys(0), mLastNoteNumber(-1), mLastFrequency(-1.0), mLastVelocity(0), mOffset(0) { for (int i = 0; i < keyCount; i++) { mKeyStatus[i] = false; } }; // Returns true if the key with a given index is currently pressed inline bool getKeyStatus(int keyIndex) const { return mKeyStatus[keyIndex]; } // Returns the number of keys currently pressed inline int getNumKeys() const { return mNumKeys; } // Returns the last pressed note number inline int getLastNoteNumber() const { return mLastNoteNumber; } inline double getLastFrequency() const { return mLastFrequency; } inline int getLastVelocity() const { return mLastVelocity; } void advance(); void onMessageReceived(IMidiMsg* midiMessage); inline void Flush(int nFrames) { mMidiQueue.Flush(nFrames); mOffset = 0; } inline void Resize(int blockSize) { mMidiQueue.Resize(blockSize); } };
private
IMidiQueue
object for storing a queue of MIDI messages. We also store information about what notes are being played and how many of them are being played. Three parameters mLast...
are needed, mLast...
our plugin will be monophonic: each next note will drown out the previous ones (the so-called priority of the last note ). The noteNumberToFrequency
function converts a MIDI note number to a frequency in Hertz. We use it because the Oscillator
class works with a frequency, not a note number.public
section contains a number of inline
getters and sends Flush
and Resize
to the mMidiQueue
.Flush
body, we set mOffset
to zero. Calling mMidiQueue.Flush(nFrames)
means that we remove its part from the nFrames
of the queue with the size of nFrames
, since we have already processed the events of this part in the previous call to the advance
function. Resetting mOffset
ensures that the next time during the advance
we will also process the beginning of the queue. The words const
, after parentheses, mean that the function will not change the immutable members of its class .onMessageReceived
in MIDIReceiver.cpp : void MIDIReceiver::onMessageReceived(IMidiMsg* midiMessage) { IMidiMsg::EStatusMsg status = midiMessage->StatusMsg(); // We're only interested in Note On/Off messages (not CC, pitch, etc.) if(status == IMidiMsg::kNoteOn || status == IMidiMsg::kNoteOff) { mMidiQueue.Add(midiMessage); } }
mMidiQueue
.advance
: void MIDIReceiver::advance() { while (!mMidiQueue.Empty()) { IMidiMsg* midiMessage = mMidiQueue.Peek(); if (midiMessage->mOffset > mOffset) break; IMidiMsg::EStatusMsg status = midiMessage->StatusMsg(); int noteNumber = midiMessage->NoteNumber(); int velocity = midiMessage->Velocity(); // There are only note on/off messages in the queue, see ::OnMessageReceived if (status == IMidiMsg::kNoteOn && velocity) { if(mKeyStatus[noteNumber] == false) { mKeyStatus[noteNumber] = true; mNumKeys += 1; } // A key pressed later overrides any previously pressed key: if (noteNumber != mLastNoteNumber) { mLastNoteNumber = noteNumber; mLastFrequency = noteNumberToFrequency(mLastNoteNumber); mLastVelocity = velocity; } } else { if(mKeyStatus[noteNumber] == true) { mKeyStatus[noteNumber] = false; mNumKeys -= 1; } // If the last note was released, nothing should play: if (noteNumber == mLastNoteNumber) { mLastNoteNumber = -1; mLastFrequency = -1; mLastVelocity = 0; } } mMidiQueue.Remove(); } mOffset++; }
Peek
and Remove
). But we do this only for those MIDI messages whose offset ( mOffset
) is no larger than the buffer offset. This means that we process each message in the corresponding sample, leaving relative time shifts intact.noteNumber
and Velocity
values, the conditional if
separates the note on and note off messages (the absence of a velocity value is interpreted as note off ). In both cases, we keep track of which notes are played and how many of them are at the moment. The values ​​for mLast...
are also updated mLast...
to fulfill the priority of the last note. Further, it is logical that the frequency of the sound should be updated here, which is what we are doing. At the very end, mOffset
updated so that the recipient knows how far this message is in the buffer at the moment. This can be reported to the recipient in another way - by passing the offset as an argument. // #define PLUG_CHANNEL_IO "1-1 2-2" #if (defined(AAX_API) || defined(RTAS_API)) #define PLUG_CHANNEL_IO "1-1 2-2" #else // no audio input. mono or stereo output #define PLUG_CHANNEL_IO "0-1 0-2" #endif // ... #define PLUG_IS_INST 1 // ... #define EFFECT_TYPE_VST3 "Instrument|Synth" // ... #define PLUG_DOES_MIDI 1
0-1
and 0-2
indicate that the plug-in has no audio input and there is one output, i.e. mono ( 0-1
), or it has no audio inputs and there is a stereo output ( 0-2
).#include "MIDIReceiver.h"
after Oscillator.h
to Synthesis.h . In the same section, in the public
section, add a member function declaration: // to receive MIDI messages: void ProcessMidiMsg(IMidiMsg* pMsg);
MIDIReceiver
object in the private
section: private: // ... MIDIReceiver mMIDIReceiver;
void Synthesis::ProcessMidiMsg(IMidiMsg* pMsg) { mMIDIReceiver.onMessageReceived(pMsg); }
enums
at the top: enum EParams { kNumParams }; enum ELayout { kWidth = GUI_WIDTH, kHeight = GUI_HEIGHT };
void Synthesis::CreatePresets() { MakeDefaultPreset((char *) "-", kNumPrograms); }
void Synthesis::OnParamChange(int paramIdx) { IMutexLock lock(this); }
Synthesis::Synthesis(IPlugInstanceInfo instanceInfo) : IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo) { TRACE; IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight); pGraphics->AttachPanelBackground(&COLOR_RED); AttachGraphics(pGraphics); CreatePresets(); }
void Synthesis::Reset() { TRACE; IMutexLock lock(this); mOscillator.setSampleRate(GetSampleRate()); }
ProcessDoubleReplacing
function. Consider: mMIDIReceiver.advance()
each sample needs to be executed. After that, we will know the frequency and volume with getLastVelocity
and getLastFrequency
from the MIDI receiver. Then we call mOscillator.setFrequency()
and mOscillator.generate()
to fill the audio buffer with the sound of the desired frequency.generate
function was created to handle the entire buffer; The MIDI receiver works at the level of an individual sample: messages can have any offset within the buffer, which means mLastFrequency
can change on any sample. We'll have to refine the Oscillator
class so that it also works at the sample level.twoPI
out twoPI
from generate
and move the Oscillator.h section to private
. While we are here, let's immediately add the linden bool
variable to indicate whether the oscillator is muted (that is, not a single note is played): const double twoPI; bool isMuted;
Oscillator() : mOscillatorMode(OSCILLATOR_MODE_SINE), mPI(2*acos(0.0)), twoPI(2 * mPI), // This line is new isMuted(true), // And this line mFrequency(440.0), mPhase(0.0), mSampleRate(44100.0) { updateIncrement(); };
inline void setMuted(bool muted) { isMuted = muted; }
double nextSample();
double Oscillator::nextSample() { double value = 0.0; if(isMuted) return value; switch (mOscillatorMode) { case OSCILLATOR_MODE_SINE: value = sin(mPhase); break; case OSCILLATOR_MODE_SAW: value = 1.0 - (2.0 * mPhase / twoPI); break; case OSCILLATOR_MODE_SQUARE: if (mPhase <= mPI) { value = 1.0; } else { value = -1.0; } break; case OSCILLATOR_MODE_TRIANGLE: value = -1.0 + (2.0 * mPhase / twoPI); value = 2.0 * (fabs(value) - 0.5); break; } mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } return value; }
twoPI
used twoPI
. It would be redundant to calculate this value for each sample, so we added two pi as a constant to the class.switch
construction is already familiar to you, even though we don’t use a for
loop. Here we simply generate one value for the buffer, instead of filling it in its entirety. Also, this structure allows us to move the phase increment to the end, avoiding repetition.generate
function with a “buffer” approach. But this implementation took us less than an hour entirely. In simple applications (like this) it is sometimes more efficient to implement an approach and see how the code handles the task in practice. Most often, as we just saw, it turns out that the whole idea was correct (the principle of calculating different sound waves), but some aspect of the problem was missed. On the other hand, if you are developing a public API, then changing something later is, to put it mildly, inconvenient, so here you have to think about everything in advance. In general, it depends on the situation.setFrequency
function setFrequency
also be called every sample. So updateIncrement
will also be called very often. But it is not yet optimized: void Oscillator::updateIncrement() { mPhaseIncrement = mFrequency * 2 * mPI / mSampleRate; }
2 * mPI * mSampleRate
changes only when the sample rate changes. So the result of this calculation is better to remember and recalculate it only inside Oscillator::setSampleRate
. It is worth remembering that exorbitant optimization can make the code unreadable and even ugly. In the specific case, we will not have performance problems, since we are writing an elementary monophonic synth. When we get to polyphony, it will be another matter, and then we will definitely optimize.ProcessDoubleReplacing
in Synthesis.cpp : void Synthesis::ProcessDoubleReplacing( double** inputs, double** outputs, int nFrames) { // Mutex is already locked for us. double *leftOutput = outputs[0]; double *rightOutput = outputs[1]; for (int i = 0; i < nFrames; ++i) { mMIDIReceiver.advance(); int velocity = mMIDIReceiver.getLastVelocity(); if (velocity > 0) { mOscillator.setFrequency(mMIDIReceiver.getLastFrequency()); mOscillator.setMuted(false); } else { mOscillator.setMuted(true); } leftOutput[i] = rightOutput[i] = mOscillator.nextSample() * velocity / 127.0; } mMIDIReceiver.Flush(nFrames); }
advance
). If a note sounds ( velocity > 0
), we update the oscillator frequency and let it sound. Otherwise we drown it (then nextSample
will return zeros).nextSample
to get the value, change the volume ( velocity
is an integer between 0
and 127
) and write the result to the output buffers. At the end, Flush
is called to remove the start of the queue.PLUG_UNIQUE_ID
in resource.h . If two plugins have the same ID, the host will ignore everything except one.Source: https://habr.com/ru/post/226573/
All Articles