Creating Audio Plugins, Part 13

    All posts in the series:
    Part 1. Introduction and configuration
    Part 2. Learning the 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



    We have already created all the components of the classical subtractive synthesizer, and now, having a clear understanding of the structure, we will do the redesign.

    Our plugin will be a polyphonic synthesizer called SpaceBass (Kasmichi Bass):



    The plugin will have two oscillators, the sound of which can be mixed using the handle. Turn all the way to the left - only the first oscillator will sound, all the way to the right - the second. The position in the middle will give us the sounds of both oscillators in equal proportion. The pitch mod knob adjusts the modulation level of the tone of the corresponding oscillator with a low-frequency oscillator. We already figured out the volume and filter envelopes. The “LFO amount” knob appeared in the filter section. It controls how the LFO affects the cutoff frequency.

    Even at this stage, it looks good. Let's evaluate the progress. Here is what we have already done:

    • MIDI receiver
    • Virtual keyboard
    • Oscillator
    • Volume envelope
    • Multi-mode filter
    • Filter envelope
    • Lfo


    Here is a list of what remains to be done:

    • New design
    • Polyphony
    • Mixing oscillator sounds (it's easy)
    • Oscillator tone modulation


    As you can see, most have already been done!
    The plan is this: first, take a look at how graphic design is made in Photoshop. Then the main part is polyphony, it will slightly change the structure of the plugin. At the end, add tone modulation.

    We will divide these tasks into three posts. This one will be about design, the other two - about polyphony, since this is a rather laborious part.

    New design



    The handle is a slightly modified Boss-like Knob of Leslie Sanford . I just made it smaller, extended the notch and slightly changed the shadow. The modified version can be downloaded from here . I made a new keyboard using this guide .

    I will not go into details of where to get photoshop and how to draw all of it in it, but we will look at the structure of the layers and the general principles of work. Download the archived TIF , unzip it and open it in Photoshop or GIMP. I tried to leave the structure untouched so that I could dig into it and understand how it was made. If you want to change the text, you will need a couple of fonts: Tusj for the logo and Avenir 85 Heavyfor signatures. Unfortunately, the second one is no longer free, but you can use Helvetica, for example, instead. Download and install fonts in your system.

    Smart objects and vector shapes.



    Studying the structure of layers in Photoshop, you can see that most of the objects are vector shapes and smart objects . I highly recommend using smart objects in the following cases:

    • When the same component is needed in several places
    • If you want to apply some effect to a group of objects (and use a version of Photoshop younger than CS6)
    • You want to rotate an object without losing information, with the ability to rotate it back


    The first item relates to knobs and waveform switches. They appear in several places and their instances look identical. Select the Move tool (by pressing V ) and, holding Cmd on Mac or Ctrl on Windows, click on the waveform switch. The layer called Shapes is highlighted in the layers palette . Double-click on the preview to open the smart object. It will become clear that waveforms are vector objects:



    The advantage of vector objects is that they can be rotated, scaled and modified in any way as much as desired and without loss. So if, for example, we need a new version of the interface for screens with a high pixel density, we will only need to scale these pictures. Yes, we would have to export a pen from JKnobMan, but since all the pens are the same smart object, replacing them will be very simple.

    Let's dissect the Keyboard layer group . Inside you can find four octaves. Octave is also a smart object, all changes made over one octave will affect others. Open, for example, Octave 4 and take a look at the palette:



    All black keys point to one smart object, and white too. Double-click on any black button in the layers palette. It will become clear that it consists of three vector forms:



    Imagine now that we would like to smooth out the corners of the black keys a bit. If we riveted copies, we would have to rake it all now. But with this approach, it all comes down to editing a single key.

    Using Photoshop, adhere to several principles:

    • Wherever possible, work non-destructively. Use smart filters , do not scale raster objects, avoid rasterizing layers. When you rasterize something, all information about the original structure of the object is lost . If you still have to rasterize, first make a backup of the vector form to return to it if necessary.
    • Avoid copying with smart objects. Before you copy something, ask yourself: will you modify the copy? If not, then you need not a dumb copy, but a smart object.
    • Use the Pen tool to edit objects such as rounded rectangles, for example. This tool allows you to save places such as curves.


    A couple of useful points to create your own virtual keyboard:

    • When drawing unpressed keys, remember that when pressed, they usually should look darker. If the black key is literally black in the unpressed state, it will be difficult to depict it “more pressed”.
    • All white keys are the same width as black ones.
    • In the pressed form Do and Fa (C and F), just like Mi and C (E and B) - this is the same image. Make sure that the sharps of the first and the flats of the second have the same offset, otherwise layering and gaps will appear.
    • When drawing keystrokes, do not use translucent overlays. Use mask layers so that each key pressed changes only its image area. Effects such as shadows and an external glow using IKeyboardControlcorrectly display will not work.
    • The highest key for IKeyboardControlthis is Up. So you need a white key without a black one on top. If you work non-destructively, it will not be any work.


    Another small hint: in the logo of our synth, the letter S occurs three times. The letters in this font have an irregular texture, so the three absolutely identical letters S look completely unnatural. To make the logo look better, use an additional layer with a mask, with which you can slightly change the texture of the same letters in several places.

    Graphics Export



    Interface components need to be exported as separate .png in order to refer to them from the plugin code. You can export manually or download the following six files:



    GUI Implementation



    As usual, copy the Synthesis project using the good old super-useful duplicate script. Run it from the IPlugExamples folder :

    cd ~/plugin-development/wdl-ol/IPlugExamples/
    ./duplicate.py Synthesis/ SpaceBass YourName

    If you have not followed the development in previous posts, you can download the project , unzip it, and then run the script duplicate.

    Copy all six images to the project resources folder and delete knob_small.png , which we no longer need.

    Since we use the same file names, we only need to slightly modify resource.h . Remove KNOB_SMALL_IDand KNOB_SMALL_FN. The file header should look something 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"
    


    The interface has become a bit larger:

    // GUI default dimensions
    #define GUI_WIDTH 571
    #define GUI_HEIGHT 500
    


    We need to edit another resource file, SpaceBass.rc :

    #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 change the class a bit Oscillator. Let's move it enum OscillatorModeinside the class so that you can access it from the outside like Oscillator::OscillatorMode. We did the same in Filterand EnvelopeGeneratorwill do here, “for symmetry.”
    Change the order of the sections publicand privateto publicgo first. And move enum OscillatorModeup this section:

    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();
    };
    


    Now let's proceed directly to the GUI code. Let's start with SpaceBass.h . Add a couple of privatefeatures:

    void CreateParams();
    void CreateGraphics();
    


    Thus, we do not overwhelm the constructor with interface code. While we are there, delete what we don’t need anymore double mFrequency.
    Now in SpaceBass.cpp before enum EParamsadd the constant:

    const double parameterStep = 0.001;
    


    This parameter determines the accuracy with which the user can turn the interface knob. It is used in each pen, so it’s a good idea to create one constant here instead of writing specific values ​​in each separate line with the parameters of the handles.
    The new version of our plugin now has more options. Edit EParams:

    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
    };
    


    And ELayoutalso, since the location of the virtual keyboard has changed:

    enum ELayout
    {
        kWidth = GUI_WIDTH,
        kHeight = GUI_HEIGHT,
        kKeybX = 62,
        kKeybY = 425
    };
    


    All parameters in one place



    Instead of churning out the challenges InitDouble()and new IKnobMultiControl, better to create a special data structure for storing GUI information.
    Create the following structunder EParams:

    typedef struct {
        const char* name;
        const int x;
        const int y;
        const double defaultVal;
        const double minVal;
        const double maxVal;
    } parameterProperties_struct;
    


    It stores the name of the parameter, the coordinates of the control in the plug-in window and the default / minimum / maximum values ​​(if the type parameter double). For switches, we do not need parameters default/min/maxVal. Due to static typing, this would be an extra twist.
    Below we create a data structure that (almost) stores parameter data. For each parameter we need one parameterProperties_struct, which means we need an array of size kNumParams:

    const parameterProperties_struct parameterProperties[kNumParams] =
    


    Insert real values ​​under this line. Note that the values default/min/maxValsremain uninitialized for type parameters enumsuch as 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}
    };
    


    Massive contraption. A similar syntax with curly braces {}is a relatively new technique in C / C ++ called “ compound literals ”. The basic idea is that structures and arrays can be initialized in this way. External brackets initialize the array parameterProperties[]; they contain a comma-separated list of compound literals, each of which initializes one parameterProperties_struct. Let's look at this using the first literal as an example:

    {.name="Osc 1 Waveform", .x=30, .y=75}
    


    An old-school approach would be to write this:

    parameterProperties_struct* osc1Waveform_prop = parameterProperties[mOsc1Waveform];
    osc1Waveform_prop->name = "Osc 1 Waveform";
    osc1Waveform_prop->x = 30;
    osc1Waveform_prop->y = 75;
    


    This would have to be done for each parameter!
    The “classic” approach to composite literals for structures structlooks like this:

    {"Osc 1 Waveform", 30, 75}
    


    Very concise, but error prone. If you add something to the beginning structor change the order of the elements, problems will appear. It is better to use the designated initializers , although you will have to type more. This monstrous phrase simply means that you can access the elements structusing the syntax .membername=. In its final form, this is a bit like JSON or hashes in Ruby :

    {.name="Osc 1 Waveform", .x=30, .y=75}
    


    Create Parameters



    We have added two member functions CreateParamsand CreateGraphics. Now the constructor looks 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);
    }
    


    Everything is transparent, right? Instead of piling up GetParam(), and pGraphicshere we have brought all this outside.
    Let's write 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;
            }
        }
    


    We iterate over all parameters. First, we get the necessary properties from the data structure that we just created, then we switchinitialize the different ones with the help enum. For LFO, the triangular waveform is selected by default, simply because it is it that is most often used. Please note that for all sixteen pens we use only one expression!
    For some pens, it is better to specify non-linear behavior. For example, it is better to change the cutoff frequency logarithmically, due to the mathematical relationship between notes in octaves and their frequencies. Add the appropriate calls SetShapeto the end 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);
    


    Finally, for each parameter, you need to call it once OnParamChange, so that when you first call the plugin, the internal variables have the correct values:

        for (int i = 0; i < kNumParams; i++) {
            OnParamChange(i);
        }
    }
    


    With the internal parameters finished, now we will add controls for them. This is done in the body CreateGraphics. First, add a background image:

    void SpaceBass::CreateGraphics() {
        IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight);
        pGraphics->AttachBackground(BG_ID, BG_FN);
    


    Then the 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 in the keyboard is the number of octaves (now only four) and keyCoordinates. The new keys are wider, so you need to adjust the step between them so that the pressed keys appear at adequate coordinates.
    Next, upload the images of the knobs and switches:

        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);
    


    As usual, here we simply load .png and tell the system how many frames are in each of them.
    The main part is iterating over all parameters to create the appropriate controls:

       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);
        }
    


    Here we first find out the properties for the current parameter, then use switchfor specific cases. Also here, instead of waveform.png , filtermode.png is used for the mFilterMode parameter . Again, default is the code for the handle, because the handle is most often found among controls.
    We complete the function body by calling AttachGraphics:

        AttachGraphics(pGraphics);
    }
    


    Finally, uninstall switchfrom OnParamChange()in SpaceBass.cpp . We will rewrite it next time.

    Done!



    Launch the plugin and enjoy the new interface in all its glory! True, the sound does not work yet - we will do it next time. We have to make the synthesizer polyphonic!

    The code can be downloaded from here .
    Original post .

    Also popular now: