Pull to refresh

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

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



Настройка виртуальной клавиатуры в REAPER не так очевидна, к тому же хост пользователя может вообще не иметь такой функциональности. Давайте добавим свою маленькую экранную клавиатуру в GUI.


Элемент GUI



В WDL-OL элементы GUI называются controls. В этой библиотеке есть класс IkeyboardControl, обладающий всей необходимой функциональностью для этой задачи.
Он использует одну фоновую картинку и два дополнительных спрайта: в одном изображение нажатой черной клавиши, в другом несколько нажатых белых. Логично: все черные клавиши имеют одинаковую форму, тогда как белые бывают разными. При нажатии на клавиши эти спрайты будут отображаться поверх фонового изображения клавиатуры, которое будет видно всегда.
Если вы хотите нарисовать свои замечательные кастомные клавиши, пробегитесь по этому руководству. Ну а те, что идут с библиотекой, выглядят так:







Скачайте эти файлы и закиньте в папку проекта /resources/img/. Если пользуетесь Xcode, перетяните их в окно для добавления в проект. Как обычно, работа с графикой начинается с добавления имен файлов в resource.h. Заодно, пока вы там, удалите ссылки на knob.png и background.png, и удалите сами файлы из проекта.

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


Понадобится больший размер окна:

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


На Windows для включения .png файлов в сборку также необходимо отредактировать заголовок Synthesis.rc:

#include "resource.h"

BG_ID       PNG BG_FN
WHITE_KEY_ID       PNG WHITE_KEY_FN
BLACK_KEY_ID       PNG BLACK_KEY_FN


Теперь в секции public файла Synthesis.h добавим несколько членов класса Synthesis:

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;


В список инициализации в Synthesis.cpp нужно добавить lastVirtualKeyboardNoteNumber:

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


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

В секцию private тоже надо добавить пару строк:

IControl* mVirtualKeyboard;
void processVirtualKeyboard();


Класс IControl является базовым для всех элементов управления GUI. Мы не можем объявить здесь объект IkeyboardControl, т. к. он «не известен» .h файлам. Поэтому нам придется использовать указатели. В IKeyboardControl.h есть комменты, в которых сказано: «следует добавлять (#include) этот хедер после объявления класса вашего плагина, так что лучше всего добавить его в главный .cpp файл плагина».
Чтобы прояснить ситуацию, давайте посмотрим на Synthesis.cpp. Добавьте #include "IKeyboardControl.h" перед #include resource.h.
Теперь измените конструктор:

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


Интересные вещи начинаются, когда мы прикрепляем фоновую картинку. Сначала мы подгружаем нажатые черные и белые клавиши в виде объектов Ibitmap. Третий аргумент функции LoadIBitmap (6) сообщает графической системе что в whitekeys.png содержится шесть кадров:

По умолчанию pRegularKeys должен содержать 6 изображений (C/F, D, E/B, G, A, верхняя C), в то время как pSharpKey содержит только 1 картинку (для всех бемолей/диезов).
— IKeyboardControl.h

Массив keyCoordinates сообщает системе смещение каждой клавиши относительно левой границы. Это действие нужно проделать только с одной октавой, а IKeyboardControl вычислит смещения для всех остальных октав.
В следующей строке мы, грубо говоря, инициализируем новый объект new IKeyboardControl и назначаем ему имя mVirtualKeyboard. Мы передаем массу информации:
  • Указатель на экземпляр плагина. Это пример шаблона делегирования: виртуальная клавиатура будет вызывать GetNumKeys и GetKeyStatus для этого экземпляра (this);
  • Координаты клавиатуры в GUI;
  • Номер самой низкой ноты. При клике на крайнюю слева клавишу именно эта нота будет проигрываться;
  • Количество октав;
  • Ссылки на картинки нажатых клавиш;
  • Относительную координату X внутри одной октавы;

Интересно, что объект виртуальной клавиатуры даже не знает о существовании файла bg.png. Он ему просто не нужен, все и так будет работать. Это плюс, так как изображение клавиатуры может быть частью фоновой картинки, и тогда пришлось бы вырезать этот кусок только чтобы передать его конструктору IkeyboardControl.

Если у вас есть опыт программирования в C++, то должен возникнуть условный рефлекс: конструктор содержит new, значит в деструкторе необходимо написать delete mVirtualKeyboard. Но если мы это сделаем, а потом удалим плагин с трека, то выскочит исключение runtime exception. Причина в том, что когда осуществляется вызов

pGraphics->AttachControl(mVirtualKeyboard);

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

Теперь удалите тело функции CreatePresets:

void Synthesis::CreatePresets() {
}


И добавьте kKeybX и kKeybY в ELayout:

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


Из соображений производительности IKeyboardControl не перерисовывает сам себя. Это частая практика в программировании графики: пометить элемент GUI как «грязный», т. е. Его изображение будет обновлено только в следующем цикле перерисовки. Если вы взглянете на IKeyboardControl.h, в частности на OnMouseDown и OnMouseUp, вы увидите, что mKey присвоено некоторое значение и что вызывается функция SetDirty (в противоположность Draw). SetDirty это функция-член класса IControl (имплементацию которого можно найти в IControl.cpp, соответственно). Она устанавливает значение параметра mDirty данного элемента управления равным true. Каждый цикл перерисовки графическая система перерисовывает все элементы GUI, чей mDirty равен true. Я углубился в такие детали, так как важно понимать этот аспект работы графической системы.

Реакция на внешние MIDI сообщения



Пока что клавиатура становится «грязной», когда на нее нажимают. От mMIDIReceiver она получает данные о нажатых клавишах, но она также должна получать и внешние MIDI данные. mVirtualKeyboard и mMIDIReceiver ничего друг о друге не известно, так что давайте в Synthesis.cpp отредактируем ProcessMidiMsg:

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


Сначала mMIDIReceiver обновляет члены mLast... в соответствии с полученными MIDI данными. Затем mVirtualKeyboard помечается как «грязный». Тогда в следующем цикле перерисовки будет вызвана Draw для mVirtualKeyboard, который, в свою очередь, вызовет GetNumKeys и GetKeyStatus. Поначалу это может показаться заковыристым, но в действительности это прозрачный четко структурированный дизайн, позволяющий избежать избыточности и лишних движений.
Наша виртуальная клавиатура теперь реагирует на внешние MIDI сообщения и правильно рисует нажатые клавиши.

Реакция на нажатия на виртуальной клавиатуре



Осталось заставить клавиатуру реагировать на нажатия по встроенной в хост виртуальной клавиатуре, генерировать MIDI сообщения и посылать их получателю mMIDIReceiver.
Добавьте этот вызов ProcessDoubleReplacing непосредственно перед циклом for:

processVirtualKeyboard();


И напишите соответствующую функцию:

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


GetKey дает нам номер ноты, соответствующий нажатой клавише. IKeyboardControl не поддерживает мультитач, так что только одна клавиша может быть нажата за раз. Первый if отпускает клавишу, на которую больше не жмут (если такая имеется). Так как эта функция вызывается каждые mBlockSize семплов, второй if гарантирует, что будет сгенерировано только одно сообщение note on для этого клика (а не через каждые mBlockSize семплов). Мы запоминаем значение lastVirtualKeyboardNoteNumber чтобы избежать этих «повторных нажатий» при каждом вызове функции.

Поехали!



Мы снова готовы запустить наш синтюк! Если все сделали правильно, можно играть на его клавиатуре. А использование виртуальной клавиатуры хоста или любого другого подключенного источника MIDI должно отображаться на клавиатуре плагина (по очереди, показывая последнюю нажатую клавишу). Правда, звук тоже будет соответствовать только этой одной последней клавише. Мы разберемся с полифонией немного позже.
Можете пока похвастаться перед друзьями и сыграть своего любимого Бетховена на классическом синтезаторном звуке. Только вот звук какой-то «деревянный», и слышны щелчки, когда нажимаешь и отпускаешь клавишу. Особенно это заметно, если генерируется синус. Значит, надо добавить огибающие. Сделаем это в следующем посте.

Файлы проекта на данном этапе можно скачать отсюда.

Оригинал статьи:
martin-finke.de/blog/articles/audio-plugins-010-virtual-keyboard
Tags:
Hubs:
+23
Comments 10
Comments Comments 10

Articles