REAPER’s virtual keyboard is a little laborious to set up, and your customers may not always have a host with such functionality. Let’s add a little on-screen keyboard to our plugin’s GUI.
The GUI Element
In WDL-OL, GUI elements are called controls. WDL-OL comes with an IKeyboardControl
which has all the functionality we need. It uses a background graphic and two sprites: One is a pressed black key, the other contains several pressed white keys. The reason for this is that all black keys have the same shape, but the white keys have different shapes. Initially, only the background will be visible. When a key is played, the pressed key graphic will be overlaid on top at the appropriate position.
If you are interested in creating a beautiful piano graphic yourself, check out this tutorial. Anyway, here are the three files that come with WDL-OL:
Download all three, put them in your project’s /resources/img/ folder. Then drag them into Xcode to add them to the project. As usual with graphics, we’ll first add the filename to resource.h. While you’re there, remove the knob.png and background.png references and remove the two files from your 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"
Also change the GUI size:
// GUI default dimensions
#define GUI_WIDTH 434
#define GUI_HEIGHT 66
To have the png files included in windows builds, edit Synthesis.rc and modify the beginning to this:
#include "resource.h"
BG_ID PNG BG_FN
WHITE_KEY_ID PNG WHITE_KEY_FN
BLACK_KEY_ID PNG BLACK_KEY_FN
Now add some public members to the Synthesis
class (in Synthesis.h):
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;
Initialize lastVirtualKeyboardNoteNumber
in the initializer list (in Synthesis.cpp):
Synthesis::Synthesis(IPlugInstanceInfo instanceInfo)
: IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo),
lastVirtualKeyboardNoteNumber(virtualKeyboardMinimumNoteNumber - 1) {
// ...
}
When MIDI notes are played from the host, they should be visible as pressed keys on our virtual keyboard. The virtual keyboard will call getNumKeys
and getKeyStatus
to find out which keys are currently being pressed. We have already implemented these functions on the MIDIReceiver
, so we’re just passing it on.
The private
section needs two additions:
IControl* mVirtualKeyboard;
void processVirtualKeyboard();
The IControl
class is the base class of all the GUI controls. We can’t declare an instance of IKeyboardControl
here because it isn’t known in header files. For that reason, we have to use a pointer. IKeyboardControl.h has some comments saying that you “should include this header file after your plug-in class has already been declared, so it is propbably best to include it in your plug-in’s main .cpp file”.
To make this a little more clear, let’s go into Synthesis.cpp. Add #include "IKeyboardControl.h"
right before you #include resource.h
. Now modify the constructor as follows:
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();
}
The interesting part begins after we have attached the background graphic. First we load the pressed black/white keys as IBitmap
s. The second argument (6
) to LoadIBitmap
tells the graphics system that whitekeys.png contains six frames:
By default pRegularKeys should contain 6 bitmaps (C/F, D, E/B, G, A, high C), while pSharpKey should only contain 1 bitmap (for all flat/sharp keys).
The keyCoordinates
array tells the system how far each key is offset from the left. Note that you only have to do this for one octave; IKeyboardControl
will infer the coordinates for all other octaves.
On the next line, we assign a new IKeyboardControl
to mVirtualKeyboard
. We pass a lot of information:
- A pointer to our plugin instance. This is an example of the delegate pattern: The virtual keyboard will call
GetNumKeys
andGetKeyStatus
onthis
. - The keyboard’s X and Y coordinates on the GUI.
- The lowest note number. When you click the leftmost key, this note will be played.
- The number of octaves
- The addresses of our two pressed key images
- The X coordinate of each key in one octave
Interestingly, the virtual keyboard knows nothing about bg.png. It doesn’t need it! This is good because the keyboard may be part of one big background bitmap and it would be annoying to cut out the keyboard part just to pass it to the IKeyboardControl
constructor. It just acts when keys are pressed.
If you have some C++ experience, writing new
in the constructor may (and should) urge you to put delete mVirtualKeyboard
in the destructor. If you do that and unload your plugin (i.e. remove it from a track), you’ll get a runtime exception. The reason is that when you call:
pGraphics->AttachControl(mVirtualKeyboard);
You’re passing ownership to the graphics system. This means that the memory management is no longer your responsibility, and using delete
will try to deallocate memory that has already been deallocated.
Now empty the CreatePresets
function:
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, the IKeyboardControl
doesn’t redraw itself just by itself. A common pattern in graphics programming is to mark a GUI component as dirty, which means that it will be redrawn on the next paint cycle. If you look in IKeyboardControl.h, particularly OnMouseDown
and OnMouseUp
, you’ll see that mKey
is set to some value and SetDirty
is called (as opposed to Draw
). SetDirty
is an IControl
member function (found in IControl.cpp) that sets the control’s mDirty
member to true
. On every paint cycle, the graphics system repaints all controls whose mDirty
is true
. I’m going into such detail here because this is an important aspect of how the graphics system works.
Reacting to External MIDI
Until now, the keyboard marks itself dirty only when it’s clicked. It gets the status of pressed keys from the mMIDIReceiver
, but it has to be informed when external MIDI is received. mVirtualKeyboard
and mMIDIReceiver
know nothing about each other, so we’ll modify ProcessMidiMsg
(in Synthesis.cpp):
void Synthesis::ProcessMidiMsg(IMidiMsg* pMsg) {
mMIDIReceiver.onMessageReceived(pMsg);
mVirtualKeyboard->SetDirty();
}
First, mMIDIReceiver
can update its mLast...
members according to the received MIDI data. Then, mVirtualKeyboard
is marked as dirty. So on the next paint cycle, the renderer will call Draw
on mVirtualKeyboard
, which will call GetNumKeys
and GetKeyStatus
. This may sound a little indirect at first, but it’s a clean design that keeps components separate and avoids redundant work.
Our virtual keyboard now reacts to external MIDI input and shows the appropriate keys as being pressed.
Reacting to Virtual Key Presses
The last part that’s missing is the opposite direction: Reacting to clicks on the virtual keyboard, generating MIDI messages and passing them to mMIDIReceiver
.
Add the following call to ProcessDoubleReplacing
, right before the for
loop:
processVirtualKeyboard();
And implement the 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;
}
After a cast, we get the pressed key’s MIDI note number using GetKey
. IKeyboardControl
doesn’t support multi-touch, so only one key can be clicked at once. The first if
statement releases a key that is no longer clicked (if any). Since this function is called every mBlockSize
samples, the second if
ensures that clicking a key will only generate one note on message for a given click, and not one every mBlockSize
samples. We’re remembering the lastVirtualKeyboardNoteNumber
to avoid this kind of “re-triggering” on every call.
Showtime!
We’re ready to run our plugin again! You should be able to play notes using the plugin’s virtual keyboard. Using REAPER’s virtual keyboard (or any other MIDI input) should make the plugin’s GUI show the appropriate keys (plural) as being pressed. You will only hear a tone for the last-pressed key, though. We will address polyphony in a later post.
We can play our favourite Beethoven with the sound of classic analogue waveforms! But the sound is a little “static” and you can hear click sounds when you press and release a key (especially using the sine waveform). So the next thing to do is to add envelopes. You can download the current source files here.