We have created the components needed for a classic subtractive synth. We will now redesign the plugin with a clear concept in mind.
Our plugin will be a polyphonic subtractive synth called SpaceBass:
The plugin will have two oscillators that can be mixed using the “mix” knob. All the way to the left you’ll only hear oscillator one, all the way to the right only oscillator two. At the twelve o’clock position they’ll be mixed fifty-fifty. The “pitch mod” knobs control how much the oscillator’s pitch is affected by the LFO. The volume and filter envelope are just the ones we’ve already implemented and used. The filter section has an added knob labelled “LFO amount”. This knob controls how much the LFO will affect the filter’s cutoff frequency. We’re leaving a little space right of the LFO for the future.
Pretty nice, right? These are the plugin’s features we have already finished:
- MIDI Receiver
- Virtual MIDI Keyboard
- Oscillator
- Volume Envelope
- Multimode Filter
- Filter Envelope
- LFO
And this is our To-Do List for the next three posts:
- New Design
- Polyphony (i.e. playing multiple notes at one time)
- Oscillator Mix (this is easy)
- Oscillator Pitch Modulation
As you can see, the majority of features are already done! It’s a good example of how a fresh user interface can turn things around.
First we’ll look at the Design and how it’s done in Photoshop. The main part will be polyphony, because it’ll change the structure quite a bit. Finally, we’ll add pitch modulation.
This tutorial is split into three parts: This post about design, Polyphony Part I and Polyphony Part II. The polyphony part is split into two posts because it’ll take some work. I hope what I’ve written is clear and easy to follow. If you get stuck somewhere, please don’t hesitate to write a comment, and I’ll help you out.
A New Design
The knob is a slightly modified version of Leslie Sanford’s Boss-like Knob. I’ve just made it smaller, the line is longer and the shadow is slightly different. You can download the modified version here. The keyboard at the bottom was done using this tutorial.
I will not explain every step about how to create the GUI in Photoshop, but we’ll have a look at the various layers and how things are done. Download the zipped TIF file, unpack it and open the TIF file in Photoshop (or GIMP). I’ve tried to leave everything intact and non-destructive so you can go into the layers and see how everything’s done. If you want to change text. you’ll need two fonts: Tusj for the logo and Avenir 85 Heavy for the labels.
EDIT: Unfortunately, Avenir is not free anymore, but you can use Helvetica as a replacement.
Download the Tusj font and put it in ~/Library/Fonts/ (click here if you’re using Windows).
Smart Objects and Vector Shapes
If you browse through the layers in Photohop, you’ll notice that most things are based on vectors and smart objects. I highly recommend using smart objects whenever:
- You need the same component (e.g. the knob) in more than one place
- You want to apply effects to a group (and you’re using a Photoshop version prior to CS6)
- You want to rotate something non-destructively, i.e. when you rotate it back, there’ll be no quality loss.
The first point is true for the knob and the waveform switch. Both appear in several places and look exactly the same. Select the Move tool (by pressing V). Now click on a waveform switch while holding the Cmd key. In the layer palette a layer named Shapes will be highlighted. Double-click its preview icon to open the smart object. As you can see, the waveforms are vector shapes:
The advantage of vector shapes is that you can scale, rotate and otherwise modify them as much as you want without loss. So if at some point we need a GUI for retina displays, we can just scale the image! Of course we would have to export the knob again from JKnobMan, but since all knobs in the TIF are the same smart object, it would be very easy to replace them.
Let’s investigate the Keyboard layer group. You can see that there are four octaves. An octave is a smart object, so if we make a change to one octave, it will be reflected in all of them. Open Octave 4 (by double-clicking the layer icon). Look at the layer palette:
Here, all black keys point to the same smart object; and all white keys point to one smart object. Double-click any of the icons to open a black key. Now look at the layer palette: You can see that the black key consists of three vector shapes:
Imagine we want the black keys to have slightly more rounded edges. If we had just used duplicate layer to get all the black keys, we’d be in trouble! But with this Inception-style use of smart objects, it’s just a matter of modifying the black key once and it’ll be applied to all black keys.
Keep the following points in mind when using Photoshop:
- Work non-destructively whenever possible. Use smart filters, don’t scale bitmap content, and avoid rasterizing layers. If you rasterize something, be aware that you lose all information about how that thing was done. If you absolutely have to, make sure you leave a backup copy in case you have to go back.
- Avoid duplication by using smart objects. Whenever you duplicate something, ask yourself whether you’re going to modify the copy. If you just need a duplicate copy, you should probably convert it to a smart object first.
- Use the pen tool to resize things like rounded rectangles (and other vector shapes). That way, you’ll leave the rounded edges intact.
When you design your own virtual keyboard, here are a few hints:
- When you design the normal (non-pressed) keys, keep in mind that you’ll need darker versions for the pressed state. If your black keys look like a solid black bar in the normal state, it’ll be difficult to make them look “more pressed” than that.
- Make sure all white keys have the same width and all black keys have the same width.
- For pressed keys, C & F and E and B will have the same graphic. Make sure C♯ & F♯ and E♭ & B♭ have the same offset, otherwise you’ll get overlaps or gaps.
- When you design the pressed keys, avoid semi-transparent overlap. Use layer masks to make sure that each pressed key only affects its own area. If it has drop shadow or outer glow, they won’t be visible when using
IKeyboardControl
. IKeyboardControl
expects the highest key to be a C, so you’ll need a white key without a black key on top. If you’re working non-destructively, this shouldn’t be a problem.
Here’s another small hint: The letter “S” appears three times in the “SPACE BASS” logo text. With a grungy font like this, it’s unnatural if all S’s have the exact same dirt around them. So I used a layer mask to hide little bits of dirt here and there. Now each of them looks slightly different, which is more realistic.
Exporting Graphics
We have to export the GUI components as separate PNG files, so we can reference them from our plugin code. You can do the export by hand, or you can just download the following six files.
The background image contains all the static parts and the virtual keyboard with all keys in unpressed state:
Here are the switches for waveform and filter mode:
And the knob. I’m hiding the other knob turn states here, but they’re contained in the image file:
Recall that the IKeyboardControl
needs six pressed white keys: C/F, D, E/B, G, A and high C. Here’s the PNG with all of them below one another:
The black keys are simpler. There’s just one pressed image for all of them:
Implementing the GUI in Code
Instead of starting from scratch, let’s clone our Synthesis project. Open Terminal.app, cd
into the IPlugExamples folder and run the duplicate
script:
cd ~/plugin-development/wdl-ol/IPlugExamples/
./duplicate.py Synthesis/ SpaceBass YourName
If you haven’t followed the previous posts, you can download the Synthesis project from here. Unzip that and run the duplicate
script.
Copy all six image files you downloaded (or exported) above to the newly created SpaceBass folder and overwrite the existing files. Now open SpaceBass.xcodeproj. We don’t need the knob_small.png anymore. In the project navigator, go to Resources → img, right-click knob_small.png and choose Delete. From the popup, choose Move to Trash.
Since we’re using the same filenames as before, we only have to make two minor changes to resource.h. Remove the KNOB_SMALL_ID
and KNOB_SMALL_FN
, so the code looks like this:
// Unique IDs for each image resource.
#define BG_ID 101
#define WHITE_KEY_ID 102
#define BLACK_KEY_ID 103
#define WAVEFORM_ID 104
#define KNOB_ID 105
#define FILTERMODE_ID 106
// 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"
#define WAVEFORM_FN "resources/img/waveform.png"
#define KNOB_FN "resources/img/knob.png"
#define FILTERMODE_FN "resources/img/filtermode.png"
Also, our GUI has become a little larger. Change the dimensions to match those of bg.png:
// GUI default dimensions
#define GUI_WIDTH 571
#define GUI_HEIGHT 500
Modify the top of SpaceBass.rc to contain the new graphics:
#include "resource.h"
BG_ID PNG BG_FN
WHITE_KEY_ID PNG WHITE_KEY_FN
BLACK_KEY_ID PNG BLACK_KEY_FN
WAVEFORM_ID PNG WAVEFORM_FN
KNOB_ID PNG KNOB_FN
FILTERMODE_ID PNG FILTERMODE_FN
Now let’s make a small change to our Oscillator
class: We’re going to move the enum OscillatorMode
inside the class, so it’s accessed as Oscillator::OscillatorMode
from outside. We’ve done it like this in the Filter
and EnvelopeGenerator
classes, so it’s a good idea to change it here, too.
First, swap the public
and private
sections, so public
is before private
. Then, move the enum OscillatorMode
from outside the class to the top of the public
section. It should now look like this:
class Oscillator {
public:
enum OscillatorMode {
OSCILLATOR_MODE_SINE = 0,
OSCILLATOR_MODE_SAW,
OSCILLATOR_MODE_SQUARE,
OSCILLATOR_MODE_TRIANGLE,
kNumOscillatorModes
};
void setMode(OscillatorMode mode);
void setFrequency(double frequency);
void setSampleRate(double sampleRate);
void generate(double* buffer, int nFrames);
inline void setMuted(bool muted) { isMuted = muted; }
double nextSample();
Oscillator() :
mOscillatorMode(OSCILLATOR_MODE_SINE),
mPI(2*acos(0.0)),
twoPI(2 * mPI),
isMuted(true),
mFrequency(440.0),
mPhase(0.0),
mSampleRate(44100.0) { updateIncrement(); };
private:
OscillatorMode mOscillatorMode;
const double mPI;
const double twoPI;
bool isMuted;
double mFrequency;
double mPhase;
double mSampleRate;
double mPhaseIncrement;
void updateIncrement();
};
Let’s get busy with the actual GUI code! Starting in SpaceBass.h, add the following private
member functions:
void CreateParams();
void CreateGraphics();
These are just to remove all the GUI code from the constructor. While you’re there, remove the double mFrequency
if you like. We don’t need it anymore. Now let’s move on to SpaceBass.cpp. Right above enum EParams
, add the following constant:
const double parameterStep = 0.001;
This is the “precision” at which the user can move a knob in the GUI. It’s used for every knob, so it’s a good idea to create a constant instead of hardcoding it all over the place.
Our plugin has more parameters than before. Change EParams
to this:
enum EParams
{
// Oscillator Section:
mOsc1Waveform = 0,
mOsc1PitchMod,
mOsc2Waveform,
mOsc2PitchMod,
mOscMix,
// Filter Section:
mFilterMode,
mFilterCutoff,
mFilterResonance,
mFilterLfoAmount,
mFilterEnvAmount,
// LFO:
mLFOWaveform,
mLFOFrequency,
// Volume Envelope:
mVolumeEnvAttack,
mVolumeEnvDecay,
mVolumeEnvSustain,
mVolumeEnvRelease,
// Filter Envelope:
mFilterEnvAttack,
mFilterEnvDecay,
mFilterEnvSustain,
mFilterEnvRelease,
kNumParams
};
The location of the virtual keyboard has changed, too. Change ELayout
:
enum ELayout
{
kWidth = GUI_WIDTH,
kHeight = GUI_HEIGHT,
kKeybX = 62,
kKeybY = 425
};
All Parameters in One Place
Instead of having a lot of InitDouble()
and new IKnobMultiControl
calls like before, let’s move all information about our GUI to its own data structure. Add this struct
below EParams
:
typedef struct {
const char* name;
const int x;
const int y;
const double defaultVal;
const double minVal;
const double maxVal;
} parameterProperties_struct;
So this contains the parameter name
, the gui coordinates of the knob (or switch), and default/min/max values (if the parameter is a double
). For the three switches, we’re not going to use default
/min
/maxVal
. Because of the static typing, that would make things overly complicated.
Below that, we’ll create a data structure that holds (almost) our parameter data. We need one parameterProperties_struct
for every parameter, so it’ll be an array of length kNumParams
:
const parameterProperties_struct parameterProperties[kNumParams] =
Below that, add the actual values. Note that we’re leaving the default
/min
/maxVal
s uninitialized for the enum
type parameters like Filter Mode:
{
{.name="Osc 1 Waveform", .x=30, .y=75},
{.name="Osc 1 Pitch Mod", .x=69, .y=61, .defaultVal=0.0, .minVal=0.0, .maxVal=1.0},
{.name="Osc 2 Waveform", .x=203, .y=75},
{.name="Osc 2 Pitch Mod", .x=242, .y=61, .defaultVal=0.0, .minVal=0.0, .maxVal=1.0},
{.name="Osc Mix", .x=130, .y=61, .defaultVal=0.5, .minVal=0.0, .maxVal=1.0},
{.name="Filter Mode", .x=30, .y=188},
{.name="Filter Cutoff", .x=69, .y=174, .defaultVal=0.99, .minVal=0.0, .maxVal=0.99},
{.name="Filter Resonance", .x=124, .y=174, .defaultVal=0.0, .minVal=0.0, .maxVal=1.0},
{.name="Filter LFO Amount", .x=179, .y=174, .defaultVal=0.0, .minVal=0.0, .maxVal=1.0},
{.name="Filter Envelope Amount", .x=234, .y=174, .defaultVal=0.0, .minVal=-1.0, .maxVal=1.0},
{.name="LFO Waveform", .x=30, .y=298},
{.name="LFO Frequency", .x=69, .y=284, .defaultVal=6.0, .minVal=0.01, .maxVal=30.0},
{.name="Volume Env Attack", .x=323, .y=61, .defaultVal=0.01, .minVal=0.01, .maxVal=10.0},
{.name="Volume Env Decay", .x=378, .y=61, .defaultVal=0.5, .minVal=0.01, .maxVal=15.0},
{.name="Volume Env Sustain", .x=433, .y=61, .defaultVal=0.1, .minVal=0.001, .maxVal=1.0},
{.name="Volume Env Release", .x=488, .y=61, .defaultVal=1.0, .minVal=0.01, .maxVal=15.0},
{.name="Filter Env Attack", .x=323, .y=174, .defaultVal=0.01, .minVal=0.01, .maxVal=10.0},
{.name="Filter Env Decay", .x=378, .y=174, .defaultVal=0.5, .minVal=0.01, .maxVal=15.0},
{.name="Filter Env Sustain", .x=433, .y=174, .defaultVal=0.1, .minVal=0.001, .maxVal=1.0},
{.name="Filter Env Release", .x=488, .y=174, .defaultVal=1.0, .minVal=0.01, .maxVal=15.0}
};
What a monster! The {}
brace syntax is (relatively) newschool C/C++ and is called a compound literal. Basically you can initialize struct
s and C arrays like this. So the outer braces initialize the parameterProperties[]
array. They contain a comma-separated list of compound literals. Each of these initializes one parameterProperties_struct
. Let’s look at the first of them:
{.name="Osc 1 Waveform", .x=30, .y=75}
In oldschool C, we’d have to write this:
parameterProperties_struct* osc1Waveform_prop = parameterProperties[mOsc1Waveform];
osc1Waveform_prop->name = "Osc 1 Waveform";
osc1Waveform_prop->x = 30;
osc1Waveform_prop->y = 75;
And we’d have to do that for each parameter!
The “classic” way to use compound literals for struct
s is:
{"Osc 1 Waveform", 30, 75}
Okay, this is nice and short, but also error-prone. If we add something to the beginning of struct
or change the order of members, we’ll be in trouble. A better way (though it’s more code to type) is to use designated initializers. This just means accessing struct
members with the .membername=
notation. It gives us the final form, which looks a little like JSON or Ruby Hashes:
{.name="Osc 1 Waveform", .x=30, .y=75}
Creating the Parameters
We have added the two member functions CreateParams
and CreateGraphics
. Our constructor now becomes very simple:
SpaceBass::SpaceBass(IPlugInstanceInfo instanceInfo) :
IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo),
lastVirtualKeyboardNoteNumber(virtualKeyboardMinimumNoteNumber - 1)
{
TRACE;
CreateParams();
CreateGraphics();
CreatePresets();
mMIDIReceiver.noteOn.Connect(this, &SpaceBass::onNoteOn);
mMIDIReceiver.noteOff.Connect(this, &SpaceBass::onNoteOff);
mEnvelopeGenerator.beganEnvelopeCycle.Connect(this, &SpaceBass::onBeganEnvelopeCycle);
mEnvelopeGenerator.finishedEnvelopeCycle.Connect(this, &SpaceBass::onFinishedEnvelopeCycle);
}
Pretty straightforward, isn’t it? Instead of doing the GetParam()
and pGraphics
stuff here, we’re moving it out.
Let’s implement CreateParams
!
void SpaceBass::CreateParams() {
for (int i = 0; i < kNumParams; i++) {
IParam* param = GetParam(i);
const parameterProperties_struct& properties = parameterProperties[i];
switch (i) {
// Enum Parameters:
case mOsc1Waveform:
case mOsc2Waveform:
param->InitEnum(properties.name,
Oscillator::OSCILLATOR_MODE_SAW,
Oscillator::kNumOscillatorModes);
// For VST3:
param->SetDisplayText(0, properties.name);
break;
case mLFOWaveform:
param->InitEnum(properties.name,
Oscillator::OSCILLATOR_MODE_TRIANGLE,
Oscillator::kNumOscillatorModes);
// For VST3:
param->SetDisplayText(0, properties.name);
break;
case mFilterMode:
param->InitEnum(properties.name,
Filter::FILTER_MODE_LOWPASS,
Filter::kNumFilterModes);
break;
// Double Parameters:
default:
param->InitDouble(properties.name,
properties.defaultVal,
properties.minVal,
properties.maxVal,
parameterStep);
break;
}
}
So we’re iterating over all parameters. First we get the right properties from the data structure we just created. Then we use a switch
to initialize the enum
parameters differently. We decide that the LFO should default to the triangle waveform because that’s what LFOs use most of the time. Note how the default case is just one statement for all 16 knobs!
Some of the knobs need a non-linear behaviour. For example, the filter cutoff has a logarithmic behaviour due to how octaves and frequencies work. Let’s add the appropriate SetShape
calls at the bottom of CreateParams
:
GetParam(mFilterCutoff)->SetShape(2);
GetParam(mVolumeEnvAttack)->SetShape(3);
GetParam(mFilterEnvAttack)->SetShape(3);
GetParam(mVolumeEnvDecay)->SetShape(3);
GetParam(mFilterEnvDecay)->SetShape(3);
GetParam(mVolumeEnvSustain)->SetShape(2);
GetParam(mFilterEnvSustain)->SetShape(2);
GetParam(mVolumeEnvRelease)->SetShape(3);
GetParam(mFilterEnvRelease)->SetShape(3);
And finally, we’re going to call OnParamChange
for every parameter once, so the plugin has the right internal values when it’s first loaded:
for (int i = 0; i < kNumParams; i++) {
OnParamChange(i);
}
}
We’re now done creating the internal parameters. Let’s add GUI controls for each of them. This is done in CreateGraphics
. First, we’re going to add the background image:
void SpaceBass::CreateGraphics() {
IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight);
pGraphics->AttachBackground(BG_ID, BG_FN);
Then the virtual keyboard:
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, 10, 17, 30, 35, 52, 61, 68, 79, 85, 97, 102 };
mVirtualKeyboard = new IKeyboardControl(this, kKeybX, kKeybY, virtualKeyboardMinimumNoteNumber, /* octaves: */ 4, &whiteKeyImage, &blackKeyImage, keyCoordinates);
pGraphics->AttachControl(mVirtualKeyboard);
The only thing that has changed about the virtual keyboard is the number of octaves (now only four) and the keyCoordinates
. Our keys are wider than before, so we have to adjust the spacing to make the keys appear at the right coordinates.
Below that, load the knob and switch graphics:
IBitmap waveformBitmap = pGraphics->LoadIBitmap(WAVEFORM_ID, WAVEFORM_FN, 4);
IBitmap filterModeBitmap = pGraphics->LoadIBitmap(FILTERMODE_ID, FILTERMODE_FN, 3);
IBitmap knobBitmap = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN, 64);
Here we’re just loading the .png files, and we tell the system how many frames each of them has.
The main part is to iterate over all parameters and to create the appropriate GUI control:
for (int i = 0; i < kNumParams; i++) {
const parameterProperties_struct& properties = parameterProperties[i];
IControl* control;
IBitmap* graphic;
switch (i) {
// Switches:
case mOsc1Waveform:
case mOsc2Waveform:
case mLFOWaveform:
graphic = &waveformBitmap;
control = new ISwitchControl(this, properties.x, properties.y, i, graphic);
break;
case mFilterMode:
graphic = &filterModeBitmap;
control = new ISwitchControl(this, properties.x, properties.y, i, graphic);
break;
// Knobs:
default:
graphic = &knobBitmap;
control = new IKnobMultiControl(this, properties.x, properties.y, i, graphic);
break;
}
pGraphics->AttachControl(control);
}
Just as above, we’re first getting the properties for the current parameter. In the switch
, we treat our ISwitchControl
parameters differently. We also have to use a different bitmap for our mFilterMode
GUI control (so it uses filtermode.png instead of waveform.png. Again, the default
case is a normal knob (because most of our GUI controls are knobs).
Let’s finish the function body by calling AttachGraphics
:
AttachGraphics(pGraphics);
}
Finally, delete the switch
statement from OnParamChange()
(in SpaceBass.cpp). We’re going to replace it in the next part.
Finished!
Run the plugin, and you should see our new GUI in all its glory! The audio portion doesn’t work correctly at this point, because we’re going to remake it in the next post. We’re going to make the plugin polyphonic!
You can download what we’ve done in this part here.