Creating Audio Plugins, Part 8

  • Tutorial
All posts in the series:
Part 1. Introduction and configuration
Part 2. Learning code
Part 3. VST and AU
Part 4. Digital distortion
Part 5. Presets and GUI
Part 6. Signal synthesis
Part 7. Receiving MIDI messages
Part 8. Virtual keyboard
Part 9 Envelopes
Part 10. Refinement of the GUI
Part 11. Filter
Part 12. Low-frequency oscillator
Part 13. Redesign
Part 14. Polyphony 1
Part 15. Polyphony 2
Part 16. Antialiasing



Configuring the virtual keyboard in REAPER is not so obvious, moreover, the user host may not have such functionality at all. Let's add our little on-screen keyboard to the GUI.


GUI element



In WDL-OL, GUI elements are called controls . This library has a class IkeyboardControlthat has all the necessary functionality for this task.
It uses one background image and two additional sprites: in one image of a pressed black key, in the other a few pressed white. It is logical: all black keys have the same shape, while white ones are different. When you press the keys, these sprites will be displayed over the background image of the keyboard, which will always be visible.
If you want to draw your wonderful custom keys, go over this guide . Well, those that come with the library look like this:







Download these files and drop in the project folder / resources / img /. If you use Xcode, drag them to the window to add to the project. As usual, work with graphics begins by adding file names to resource.h . At the same time, while you are there, delete the links to knob.png and background.png , and delete the files themselves from the project.

// Unique IDs for each image resource.
#define BG_ID         101
#define WHITE_KEY_ID  102
#define BLACK_KEY_ID  103
// 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"


Larger window size required:

// GUI default dimensions
#define GUI_WIDTH 434
#define GUI_HEIGHT 66


On Windows, to include .png files in the assembly, you also need to edit the Synthesis.rc header :

#include "resource.h"
BG_ID       PNG BG_FN
WHITE_KEY_ID       PNG WHITE_KEY_FN
BLACK_KEY_ID       PNG BLACK_KEY_FN


Now, in the section of publicthe Synthesis.h file, add several members of the Synthesis class :

public:
    // ...
    // Needed for the GUI keyboard:
    // Should return non-zero if one or more keys are playing.
    inline int GetNumKeys() const { return mMIDIReceiver.getNumKeys(); };
    // Should return true if the specified key is playing.
    inline bool GetKeyStatus(int key) const { return mMIDIReceiver.getKeyStatus(key); };
    static const int virtualKeyboardMinimumNoteNumber = 48;
    int lastVirtualKeyboardNoteNumber;


In the initialization list in Synthesis.cpp, add lastVirtualKeyboardNoteNumber:

Synthesis::Synthesis(IPlugInstanceInfo instanceInfo)
    :   IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo),
    lastVirtualKeyboardNoteNumber(virtualKeyboardMinimumNoteNumber - 1) {
    // ...
}


When the source of the played notes is the host, they should be displayed as pressed on the plugin keyboard. The keyboard will call getNumKeysand getKeyStatusto find out which keys are pressed. We already implemented these functions MIDIReceiverlast time, so we move on.

In the private section, you also need to add a couple of lines:

IControl* mVirtualKeyboard;
void processVirtualKeyboard();


The class IControlis the base for all GUI controls. We cannot declare an object here IkeyboardControl, because it is “not known” to .h files. Therefore, we will have to use pointers. There are comments in IKeyboardControl.h that say: “this header should be added (#include) after the declaration of the class of your plugin, so it’s best to add it to the main .cpp file of the plugin”.
To clarify the situation, let's look at Synthesis.cpp . Add #include "IKeyboardControl.h"before #include resource.h.
Now change the constructor:

Synthesis::Synthesis(IPlugInstanceInfo instanceInfo)
    :   IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo),
    lastVirtualKeyboardNoteNumber(virtualKeyboardMinimumNoteNumber - 1) {
    TRACE;
    IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight);
    pGraphics->AttachBackground(BG_ID, BG_FN);
    IBitmap whiteKeyImage = pGraphics->LoadIBitmap(WHITE_KEY_ID, WHITE_KEY_FN, 6);
    IBitmap blackKeyImage = pGraphics->LoadIBitmap(BLACK_KEY_ID, BLACK_KEY_FN);
    //                            C#     D#          F#      G#      A#
    int keyCoordinates[12] = { 0, 7, 12, 20, 24, 36, 43, 48, 56, 60, 69, 72 };
    mVirtualKeyboard = new IKeyboardControl(this, kKeybX, kKeybY, virtualKeyboardMinimumNoteNumber, /* octaves: */ 5, &whiteKeyImage, &blackKeyImage, keyCoordinates);
    pGraphics->AttachControl(mVirtualKeyboard);
    AttachGraphics(pGraphics);
    CreatePresets();
}


Interesting things begin when we attach a background image. First, we load the pressed black and white keys in the form of objects Ibitmap. The third argument to the function LoadIBitmap( 6) tells the graphics system that whitekeys.png contains six frames:

By default, pRegularKeys should contain 6 images (C / F, D, E / B, G, A, upper C), while pSharpKey contains only 1 image (for all flat / sharps).
- IKeyboardControl.h

The array keyCoordinatestells the system the offset of each key relative to the left border. This action needs to be done with only one octave, and it will IKeyboardControlcalculate the offsets for all other octaves.
In the next line, roughly speaking, we initialize a new object new IKeyboardControland assign a name to it mVirtualKeyboard. We transmit a lot of information:
  • Pointer to an instance of the plugin. This is an example of delegation template : virtual keyboard will cause GetNumKeysand GetKeyStatusfor this instance ( this);
  • Keyboard coordinates in the GUI;
  • The number of the lowest note. When clicking on the leftmost key, this particular note will be played;
  • The number of octaves;
  • Links to pictures of pressed keys;
  • Relative X coordinate within one octave;

Interestingly, the virtual keyboard object does not even know about the existence of the bg.png file . He just does not need him, everything will work this way. This is a plus, since the keyboard image can be part of the background image, and then you would have to cut out this piece only to pass it to the designer IkeyboardControl.

If you have programming experience in C ++, then a conditioned reflex should arise: the constructor contains new, so you need to write in the destructor delete mVirtualKeyboard. But if we do this and then remove the plugin from the track, a runtime exception will pop up . The reason is that when a call is made

pGraphics->AttachControl(mVirtualKeyboard);

we transfer memory management to the graphics system, and managing this memory area is no longer our responsibility. Using deletewe will try to unfasten already free area of ​​memory.

Now remove the function body CreatePresets:

void Synthesis::CreatePresets() {
}


And add kKeybX and kKeybY to ELayout:

enum ELayout
{
    kWidth = GUI_WIDTH,
    kHeight = GUI_HEIGHT,
    kKeybX = 1,
    kKeybY = 0
};


For performance reasons, IKeyboardControldoes not redraw itself. This is a common practice in graphics programming: mark a GUI element as “dirty”, that is, its image will only be updated in the next redraw cycle. If you look at IKeyboardControl.h , in particular OnMouseDownand OnMouseUp, you will see that mKeysome value is assigned and that the function is called SetDirty(as opposed to Draw). SetDirtyit is a member function of the class IControl(the implementation of which can be found in IControl.cpp , respectively). It sets the parameter value of mDirtythis control to equal true. Each redrawing cycle, the graphics system redraws all GUI elements whose mDirtyequaltrue. I delved into such details, since it is important to understand this aspect of the graphics system.

Response to external MIDI messages



So far, the keyboard is getting dirty when it is clicked. From mMIDIReceiverit it receives data about the keys pressed, but it must also receive external MIDI data. mVirtualKeyboardand mMIDIReceivernothing is known about each other, so let's edit Synthesis.cppProcessMidiMsg :

void Synthesis::ProcessMidiMsg(IMidiMsg* pMsg) {
    mMIDIReceiver.onMessageReceived(pMsg);
    mVirtualKeyboard->SetDirty();
}


First mMIDIReceiverupdates the members mLast...according to the received MIDI data. Then mVirtualKeyboardmarked as "dirty." Then in the next cycle the redraw will be called Drawfor mVirtualKeyboard, which in turn will call GetNumKeysand GetKeyStatus. At first, this may seem intricate, but in reality it is a transparent, clearly structured design that avoids redundancy and unnecessary movements.
Our virtual keyboard now responds to external MIDI messages and correctly draws the pressed keys.

Virtual Keyboard Response



It remains to force the keyboard to respond to pressing the virtual keyboard built into the host, generate MIDI messages and send them to the recipient mMIDIReceiver.
Add this call ProcessDoubleReplacingimmediately before the loop for:

processVirtualKeyboard();


And write the appropriate function:

void Synthesis::processVirtualKeyboard() {
    IKeyboardControl* virtualKeyboard = (IKeyboardControl*) mVirtualKeyboard;
    int virtualKeyboardNoteNumber = virtualKeyboard->GetKey() + virtualKeyboardMinimumNoteNumber;
    if(lastVirtualKeyboardNoteNumber >= virtualKeyboardMinimumNoteNumber && virtualKeyboardNoteNumber != lastVirtualKeyboardNoteNumber) {
        // The note number has changed from a valid key to something else (valid key or nothing). Release the valid key:
        IMidiMsg midiMessage;
        midiMessage.MakeNoteOffMsg(lastVirtualKeyboardNoteNumber, 0);
        mMIDIReceiver.onMessageReceived(&midiMessage);
    }
    if (virtualKeyboardNoteNumber >= virtualKeyboardMinimumNoteNumber && virtualKeyboardNoteNumber != lastVirtualKeyboardNoteNumber) {
        // A valid key is pressed that wasn't pressed the previous call. Send a "note on" message to the MIDI receiver:
        IMidiMsg midiMessage;
        midiMessage.MakeNoteOnMsg(virtualKeyboardNoteNumber, virtualKeyboard->GetVelocity(), 0);
        mMIDIReceiver.onMessageReceived(&midiMessage);
    }
    lastVirtualKeyboardNoteNumber = virtualKeyboardNoteNumber;
}


GetKeygives us the note number corresponding to the pressed key. IKeyboardControldoes not support multi-touch, so only one key can be pressed at a time. The first ifreleases a key that is no longer pressed (if any). Since this function is called every mBlockSizesample, the second if ensures that only one note on message will be generated for that click (and not every mBlockSizesample). We remember the value lastVirtualKeyboardNoteNumberto avoid these “repeated presses” each time the function is called.

Go!



We are ready to launch our synth again! If everything is done correctly, you can play on his keyboard. And the use of the virtual host keyboard or any other connected MIDI source should be displayed on the plugin keyboard (in turn, showing the last key pressed). True, the sound will also correspond only to this one last key. We will deal with polyphony a bit later.
You can still show off to your friends and play your favorite Beethoven on a classic synthesizer sound. Only now the sound is kind of "wooden", and clicks are heard when you press and release the key. This is especially noticeable if a sinus is generated. So, you need to add envelopes. We will do this in the next post.

Project files at this stage can be downloaded from here .

Original article:
martin-finke.de/blog/articles/audio-plugins-010-virtual-keyboard

Also popular now: