Создание аудиоплагинов, часть 13

    Все посты серии:
    Часть 1. Введение и настройка
    Часть 2. Изучение кода
    Часть 3. VST и AU
    Часть 4. Цифровой дисторшн
    Часть 5. Пресеты и GUI
    Часть 6. Синтез сигналов
    Часть 7. Получение MIDI сообщений
    Часть 8. Виртуальная клавиатура
    Часть 9. Огибающие
    Часть 10. Доработка GUI
    Часть 11. Фильтр
    Часть 12. Низкочастотный осциллятор
    Часть 13. Редизайн
    Часть 14. Полифония 1
    Часть 15. Полифония 2
    Часть 16. Антиалиасинг



    Мы уже создали все компоненты классического субтрактивного синтезатора, и теперь, имея четкое понимание структуры, займемся редизайном.

    Наш плагин будет полифоническим синтезатором под названием SpaceBass (КасмичискийБас):



    У плагина будет два осциллятора, звук которых можно будет смешивать при помощи ручки. Поворот до упора налево — будет звучать только первый осциллятор, до упора направо — второй. Положение посередине даст нам звуки обоих осцилляторов в равной пропорции. Ручка “pitch mod” регулирует уровень модуляции тона соответствующего осциллятора низкочастотным осциллятором. С огибающими громкости и фильтра мы уже разобрались. В секции фильтра появилась ручка “LFO amount”. Ею регулируется то, как LFO влияет на частоту среза.

    Даже на этом этапе выглядит неплохо. Давайте оценим прогресс. Вот что мы уже сделали:

    • получатель MIDI
    • Виртуальная клавиатура
    • Осциллятор
    • Огибающая громкости
    • Фильтр с несколькими режимами
    • Огибающая фильтра
    • LFO


    А вот список того, что еще предстоит сделать:

    • Новый дизайн
    • Полифония
    • Смешивание звуков осцилляторов (это просто)
    • Модуляция тона осцилляторов


    Как видите, большая часть уже сделана!
    План такой: сначала взглянем, как сделан в фотошопе графический дизайн. Затем основная часть — полифония, она немного изменит структуру плагина. В конце добавим модуляцию тона.

    Эти задачи мы разобьем на три поста. Этот будет про дизайн, два других — про полифонию, так как это достаточно трудоемкая часть.

    Новый дизайн



    Ручка — немного измененная Boss-like Knob Лэсли Сэнфорда. Я просто сделал ее поменьше, удлинил насечку и немного поменял тень. Измененный вариант можно скачать отсюда. Новую клавиатуру я сделал, используя вот это руководство.

    Я не буду вдаваться в детали откуда взять фотошоп и как в нем все это нарисовать, но мы посмотрим на структуру слоев и общие принципы работы. Скачайте заархивированный TIF, распакуйте и откройте в фотошопе или в GIMP. Я постарался оставить структуру нетронутой, чтобы можно было в ней покопаться и понять, как оно сделано. Если вам захочется поменять текст, то понадобится пара шрифтов: Tusj для логотипа и Avenir 85 Heavy для подписей. К сожалению, второй больше не бесплатный, но вместо него можно использовать Helvetica, например. Скачайте и установите шрифты в свою систему.

    Умные объекты и векторные фигуры



    Изучая структуру слоев в фотошопе можно заметить, что большая часть объектов это векторные формы и умные объекты. Я настоятельно рекомендую использовать умные объекты в следующих случаях:

    • Когда один и тот же компонент нужен в нескольких местах
    • Если вы хотите применить какой-либо эффект к группе объектов (и используете версию фотошопа младше CS6)
    • Вы хотите повернуть объект без потери информации, с возможностью повернуть его обратно


    Первый пункт относится к ручкам и переключателям форм волны. Они появляются в нескольких местах и их экземпляры выглядят идентично. Выберите инструмент Move (нажатием V) и, зажав Cmd на Mac или Ctrl на Windows щелкните на переключатель форм волны. В палитре слоев подсветится слой с названием Shapes. Щелкните два раза по превьюшке, чтобы открыть умный объект. Станет видно, что формы волны — это векторные объекты:



    Преимущество векторных объектов в том, что их можно вращать, масштабировать и всячески видоизменять сколько угодно и без потерь. Так что если, например, нам понадобится новая версия интерфейса для экранов с большой плотностью пикселей, надо будет только масштабировать эти картинки. Да, нам бы пришлось экспортировать из JKnobMan ручку, но так как все ручки — это один и тот же умный объект, заменить их будет очень просто.

    Давайте препарируем группу слоев Keyboard. Внутри можно найти четыре октавы. Октава это тоже умный объект, все изменения, совершенные над одной октавой, отразятся на других. Откройте, например, Octave 4 и взгляните на палитру:



    Все черные клавиши указывают на один умный объект, и белые тоже. Щелкните два раза по любой черной клавише в палитре слоев. Станет видно, что она состоит из трех векторных форм:



    Представьте теперь, что мы захотели бы немного сгладить углы черных клавиш. Если бы мы наклепали копий, пришлось бы сейчас это все разгребать. Но с таким подходом все сводится к редактированию одной-единственной клавиши.

    Используя фотошоп, придерживайтесь нескольких принципов:

    • Везде, где возможно, работайте недеструктивно. Используйте умные фильтры, не масштабируйте растровые объекты, избегайте растеризации слоев. Когда вы растеризуете что-то, теряется вся информация об оригинальной структуре объекта. Если все-таки приходится растеризовать, сделайте сначала резервную копию векторной формы, чтобы вернуться к ней при необходимости.
    • Избегайте копирования при помощи умных объектов. Прежде чем скопировать что-то, задайтесь вопросом: будете ли вы изменять копию? Если нет, то вам нужна не тупая копия, а умный объект.
    • Пользуйтесь инструментом Pen для редактирования таких объектов, как, например, прямоугольники с закругленными углами. Этот инструмент позволит сохранить такие места как закругления.


    Пара полезных моментов для создания своей виртуальной клавиатуры:

    • Рисуя ненажатые клавиши помните, что в нажатом состоянии они, как правило, должны выглядеть темнее. Если в ненажатом состоянии черная клавиша буквально черного цвета, сложно будет изобразить ее «более нажатой».
    • Все белые клавиши имеют одну ширину, как и черные.
    • В нажатом виде До и Фа (C и F), равно как Ми и Си (E и B) — это одно и то же изображение. Следите за тем, чтобы диезы первых и бемоли вторых имели одинаковое смещение, иначе появятся наслоения и зазоры.
    • При рисовании нажатых клавиш не используйте полупрозрачные наложения. Используйте слои с масками, чтобы каждая нажатая клавиша меняла только свою область изображения. Такие эффекты как тени и внешнее свечение используя IKeyboardControl корректно отобразить не получится.
    • Самая высокая клавиша для IKeyboardControl это До. Так что вам понадобится белая клавиша без черной сверху. Если работать недеструктивно, это не составит никакого труда.


    Еще одна небольшая подсказка: в логотипе нашего синтюка буква S встречается три раза. Буквы в этом шрифте имеют нерегулярную текстуру, поэтому три абсолютно идентичных буквы S выглядят совсем неестественно. Чтобы логотип смотрелся лучше, используйте дополнительный слой с маской, при помощи которого можно немного изменить текстуру одинаковых букв в нескольких местах.

    Экспорт графики



    Компоненты интерфейса нужно экспортировать как отдельные .png, чтобы ссылаться на них из кода плагина. Можете экспортировать вручную или скачать следующие шесть файлов:



    Имплементация GUI



    Как обычно, скопируем проект Synthesis при помощи старого доброго суперполезного скрипта duplicate. Запустите его из папки IPlugExamples:

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

    Если вы не следили за разработкой в предыдущих постах, можете скачать проект, разархивировать, и затем запустить скрипт duplicate.

    Скопируйте все шесть картинок в папку ресурсов проекта и удалите knob_small.png, который нам больше не нужен.

    Так как мы используем те же имена файлов, нам надо лишь немного изменить resource.h. Удалите KNOB_SMALL_ID и KNOB_SMALL_FN. Шапка файла должна выглядеть примерно так:

    // 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"
    


    Интерфейс стал немного больше:

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


    Нужно отредактировать еще один файл ресурсов, 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
    


    Теперь давайте немного изменим класс Oscillator. Перенесем enum OscillatorMode внутрь класса, чтобы можно было обращаться к нему извне как Oscillator::OscillatorMode. Мы делали так же в Filter и EnvelopeGenerator, сделаем и здесь, «для симметричности».
    Поменяйте порядок следования секций public и private, чтобы public шел первым. И переместите enum OscillatorMode вверх этой секции:

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


    Теперь приступим непосредственно к коду GUI. Начнем с SpaceBass.h. Добавьте пару private функций:

    void CreateParams();
    void CreateGraphics();
    


    Таким образом мы не заваливаем конструктор кодом интерфейса. Пока мы там, удалите ненужный нам больше double mFrequency.
    Теперь в SpaceBass.cpp перед enum EParams добавьте константу:

    const double parameterStep = 0.001;
    


    Этот параметр определяет точность, с которой пользователь может поворачивать ручку интерфейса. Он используется в каждой ручке, так что это хорошая идея — создать одну константу тут вместо того, чтобы прописывать конкретные значения в каждой отдельной строке с параметрами ручек.
    У новой версии нашего плагина теперь больше параметров. Отредактируйте 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
    };
    


    И ELayout тоже, т. к. расположение виртуальной клавиатуры изменилось:

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


    Все параметры в одном месте



    Вместо того, чтобы штамповать вызовы InitDouble() и new IKnobMultiControl, лучше создать специальную структуру данных для хранения информации о GUI.
    Создайте следующий struct под EParams:

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


    В нем хранится имя параметра, координаты элемента управления в окне плагина и умолчальные/минимальные/максимальные значения (если параметр типа double). Для переключателей нам не понадобятся параметры default/min/maxVal. Из-за статической типизации это было бы лишним наворотом.
    Ниже создадим структуру данных, которая (почти) хранит данные параметров. Для каждого параметра нам понадобится одна parameterProperties_struct, а значит, нужен массив размером kNumParams:

    const parameterProperties_struct parameterProperties[kNumParams] =
    


    Вставьте реальные значения под этой строкой. Обратите внимание, что значения default/min/maxVals остаются неинициализорованными для параметров типа enum, таких как 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}
    };
    


    Массивная штуковина. Подобный синтаксис с фигурными скобками {} это относительно новый прием в C/C++, который называется «составные литералы» Основная идея в том, что так можно инициализировать структуры и массивы. Внешние скобки инициализируют массив parameterProperties[], они содержат разделенный запятыми список составных литералов, каждый из которых инициализирует одну parameterProperties_struct. Давайте разберем это на примере первого литерала:

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


    Олдскульным подходом было бы написать так:

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


    Это пришлось бы делать для каждого параметра!
    «Классический» подход к составным литералам для структур struct выглядит так:

    {"Osc 1 Waveform", 30, 75}
    


    Очень лаконично, но подвержено ошибкам. Если добавить что-нибудь в начало struct или изменить порядок следования элементов, появятся проблемы. Лучше использовать назначенные инициализаторы, хоть и придется больше печатать. Это чудовищное словосочетание просто обозначает, что можно обращаться к элементам struct, используя синтаксис .membername=. В конечном виде это немного похоже на JSON или хэши в Ruby:

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


    Создание параметров



    Мы добавили две функции-члена CreateParams и CreateGraphics. Теперь конструктор выглядит очень просто:

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


    Все прозрачно, не так ли? Вместо нагромождения GetParam() и pGraphics здесь, мы вынесли все это вовне.
    Давайте напишем 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;
            }
        }
    


    Мы итерируем над всеми параметрами. Сначала получаем нужные свойства из структуры данных, которую только что создали, затем при помощи switch инициализируем разные enum. Для LFO формой волны по умолчанию выбрана треугольная, просто потому, что именно она чаще всего используется. Обратите внимание, что для всех шестнадцати ручек мы используем всего одно выражение!
    Для некоторых ручек лучше задать нелинейное поведение. Например, частоту среза лучше менять логарифмически, из-за математической зависимости между нотами в октавах и их частотами. Добавим соответствующие вызовы SetShape в конец 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);
    


    Наконец, для каждого параметра нужно один раз вызвать OnParamChange, чтобы при первом вызове плагина внутренние переменные имели корректные значения:

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


    Со внутренними параметрами закончили, теперь добавим для них элементы управления. Это делается в теле CreateGraphics. Для начала добавим фоновое изображение:

    void SpaceBass::CreateGraphics() {
        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, 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);
    


    Единственное, что поменялось в клавиатуре — это количество октав (теперь всего четыре) и keyCoordinates. Новые клавиши шире, так что надо настроить шаг между ними, чтобы нажатые клавиши появлялись на адекватных координатах.
    Далее, подгрузите изображения ручек и переключателей:

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


    Как обычно, здесь мы просто подгружаем .png и сообщаем системе, сколько кадров в каждом из них.
    Основная часть — итерируя над всеми параметрами создать соответствующие элементы управления:

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


    Здесь мы сначала узнаем свойства для текущего параметра, затем используем switch для конкретных случаев. Также тут вместо waveform.png используется filtermode.png для параметра mFilterMode. И снова, default представляет собой код для ручки, потому что ручка чаще всего встречается среди элементов управления.
    Завершаем тело функции вызовом AttachGraphics:

        AttachGraphics(pGraphics);
    }
    


    Наконец, удалите switch из OnParamChange() в SpaceBass.cpp. Мы перепишем ее в следующий раз.

    Готово!



    Запустите плагин и полюбуйтесь на новый интерфейс во всей красе! Правда, звук пока не работает — мы займемся этим в следующий раз. Нам предстоит сделать синтезатор полифоническим!

    Код можно скачать отсюда.
    Оригинал поста.
    Поделиться публикацией

    Похожие публикации

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое