- PVSM.RU - https://www.pvsm.ru -
Все посты серии:
Часть 1. Введение и настройка [1]
Часть 2. Изучение кода [2]
Часть 3. VST и AU [3]
Часть 4. Цифровой дисторшн [4]
Часть 5. Пресеты и GUI [5]
Часть 6. Синтез сигналов [6]
Часть 7. Получение MIDI сообщений [7]
Пока что мы генерировали только постоянную звуковую волну, которая просто звучала на заданной частоте. Давайте посмотрим, как можно реагировать на MIDI сообщения, включать и выключать генерацию волны на нужной частоте в зависимости от получаемой ноты.
Когда плагин загружен в хост, он получает все MIDI сообщения с того трека, к которому он подцеплен. Когда начинается и заканчивается воспроизведение ноты, в плагине вызывается функция ProcessMidiMsg
. Помимо ноты в MIDI сообщениях может передаваться информация о портаменто (Pitch Bend) и управляющие команды (Control Changes [8], сокращенно CC), которые могут использоваться для автоматизации параметров плагина. Функции ProcessMidiMsg
передается сообщение IMidiMsg
, которое описывает MIDI событие в своем, независимом от формата виде. В этом описании присутствуют параметры NoteNumber
и Velocity
, которые содержат информацию о высоте громкости звука нашего осциллятора.
Каждый раз, когда приходит MIDI сообщение, система уже воспроизводит аудиобуфер, заполненный ранее. Не существует способа впихнуть новое аудио точно в момент получения MIDI сообщения. Эти события надо запомнить до следующего вызова функции ProcessDoubleReplacing
. Также необходимо запомнить время получения сообщения, так что эту информацию мы оставим нетронутой для следующего заполнения буфера.
Инструментом для выполнения этих задач послужит IMidiQueue [9].
Будем использовать наш проект Synthesis. Если вы используете систему контроля версий, самое время закомитить проект. Создайте новый класс MIDIReceiver и проверьте, чтобы .cpp компилировался в каждом таргете. В MIDIReceiver.h вставьте интерфейс между #define
и #endif
:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wextra-tokens"
#include "IPlug_include_in_plug_hdr.h"
#pragma clang diagnostic pop
#include "IMidiQueue.h"
class MIDIReceiver {
private:
IMidiQueue mMidiQueue;
static const int keyCount = 128;
int mNumKeys; // how many keys are being played at the moment (via midi)
bool mKeyStatus[keyCount]; // array of on/off for each key (index is note number)
int mLastNoteNumber;
double mLastFrequency;
int mLastVelocity;
int mOffset;
inline double noteNumberToFrequency(int noteNumber) { return 440.0 * pow(2.0, (noteNumber - 69.0) / 12.0); }
public:
MIDIReceiver() :
mNumKeys(0),
mLastNoteNumber(-1),
mLastFrequency(-1.0),
mLastVelocity(0),
mOffset(0) {
for (int i = 0; i < keyCount; i++) {
mKeyStatus[i] = false;
}
};
// Returns true if the key with a given index is currently pressed
inline bool getKeyStatus(int keyIndex) const { return mKeyStatus[keyIndex]; }
// Returns the number of keys currently pressed
inline int getNumKeys() const { return mNumKeys; }
// Returns the last pressed note number
inline int getLastNoteNumber() const { return mLastNoteNumber; }
inline double getLastFrequency() const { return mLastFrequency; }
inline int getLastVelocity() const { return mLastVelocity; }
void advance();
void onMessageReceived(IMidiMsg* midiMessage);
inline void Flush(int nFrames) { mMidiQueue.Flush(nFrames); mOffset = 0; }
inline void Resize(int blockSize) { mMidiQueue.Resize(blockSize); }
};
Здесь нам нужно включить IPlug_include_in_plug_hdr.h, потому что иначе IMidiQueue.h будет создавать ошибки.
Как видите, у нас есть private
объект IMidiQueue
для хранения очереди MIDI сообщений. Еще мы храним информацию о том, какие ноты сейчас играются и сколько их всего играется. Три параметра mLast...
нужны, т. к. наш плагин будет монофоническим: каждая следующая нота будет заглушать предыдущие (т. н. приоритет последней ноты [10]). Функция noteNumberToFrequency
конвертирует номер MIDI ноты в частоту в герцах. Мы используем ее, потому что класс Oscillator
работает с частотой, а не с номером ноты.
Секция public
содержит ряд встроенных (inline
) геттеров и передает Flush
и Resize
в очередь mMidiQueue
.
В теле Flush
мы устанавливаем mOffset
равным нулю. Вызов mMidiQueue.Flush(nFrames)
означает, что мы убираем из начала очереди ее часть размером nFrames
, так как мы уже обработали события этой части в предыдущем вызове функции advance
. Обнуление mOffset
гарантирует то, что в следующий раз в процессе выполнения advance
мы тоже будем обрабатывать начало очереди. Слова const
, стоящие после скобок, означают, что функция не будет изменять неизменяемые члены своего класса [11].
Давайте добавим имплементацию onMessageReceived
в MIDIReceiver.cpp:
void MIDIReceiver::onMessageReceived(IMidiMsg* midiMessage) {
IMidiMsg::EStatusMsg status = midiMessage->StatusMsg();
// We're only interested in Note On/Off messages (not CC, pitch, etc.)
if(status == IMidiMsg::kNoteOn || status == IMidiMsg::kNoteOff) {
mMidiQueue.Add(midiMessage);
}
}
Эта функция вызывается каждый раз при получении MIDI сообщения. Нас на данный момент интересуют только сообщения note on и note off (начать/перестать играть ноту), и мы добавляем их в очередь mMidiQueue
.
Следующая интересная функция — advance
:
void MIDIReceiver::advance() {
while (!mMidiQueue.Empty()) {
IMidiMsg* midiMessage = mMidiQueue.Peek();
if (midiMessage->mOffset > mOffset) break;
IMidiMsg::EStatusMsg status = midiMessage->StatusMsg();
int noteNumber = midiMessage->NoteNumber();
int velocity = midiMessage->Velocity();
// There are only note on/off messages in the queue, see ::OnMessageReceived
if (status == IMidiMsg::kNoteOn && velocity) {
if(mKeyStatus[noteNumber] == false) {
mKeyStatus[noteNumber] = true;
mNumKeys += 1;
}
// A key pressed later overrides any previously pressed key:
if (noteNumber != mLastNoteNumber) {
mLastNoteNumber = noteNumber;
mLastFrequency = noteNumberToFrequency(mLastNoteNumber);
mLastVelocity = velocity;
}
} else {
if(mKeyStatus[noteNumber] == true) {
mKeyStatus[noteNumber] = false;
mNumKeys -= 1;
}
// If the last note was released, nothing should play:
if (noteNumber == mLastNoteNumber) {
mLastNoteNumber = -1;
mLastFrequency = -1;
mLastVelocity = 0;
}
}
mMidiQueue.Remove();
}
mOffset++;
}
Эта функция вызывается каждый семпл, пока заполняется аудиобуфер. Пока в очереди есть сообщения, мы обрабатываем их и удаляем из начала (используя Peek
и Remove
). Но мы делаем это только для тех MIDI сообщений, чье смещение (mOffset
) не больше, чем смещение буфера. Это означает, что мы обрабатываем каждое сообщение в соответствующем семпле, оставляя относительные сдвиги по времени нетронутыми.
После считывания значений noteNumber
и Velocity
условный оператор if
разделяет сообщения note on и note off (отсутствие значения velocity интерпретируется как note off). В обоих случаях мы отслеживаем, какие ноты играются и сколько их всего в данный момент. Также обновляются значения mLast...
для выполнения приоритета последней ноты. Далее, логично, что именно здесь должна обновляться частота звука, что мы и делаем. В самом конце mOffset
обновляется, чтобы получатель знал, насколько далеко в буфере это сообщение находится в данный момент. Сообщить это получателю можно и другим способом — передав смещение как аргумент.
Итак, у нас есть класс, который получает все входящие MIDI сообщения note on/off. Он отслеживает, какие ноты сейчас играются, какая последняя нота и какая у нее частота. Давайте его используем.
Для начала аккуратно внесите эти изменения в resource.h:
// #define PLUG_CHANNEL_IO "1-1 2-2"
#if (defined(AAX_API) || defined(RTAS_API))
#define PLUG_CHANNEL_IO "1-1 2-2"
#else
// no audio input. mono or stereo output
#define PLUG_CHANNEL_IO "0-1 0-2"
#endif
// ...
#define PLUG_IS_INST 1
// ...
#define EFFECT_TYPE_VST3 "Instrument|Synth"
// ...
#define PLUG_DOES_MIDI 1
Эти строки сообщают хосту, что наш плагин «умеет миди». 0-1
и 0-2
обозначают, что у плагина нет аудиовхода и есть один выход, т.е. моно (0-1
), либо у него нет аудиовходов и есть стереовыход (0-2
).
Теперь добавьте #include "MIDIReceiver.h"
после Oscillator.h
в Synthesis.h. Там же, в секции public
, добавьте объявление функции-члена:
// to receive MIDI messages:
void ProcessMidiMsg(IMidiMsg* pMsg);
Добавьте объект MIDIReceiver
в секции private
:
private:
// ...
MIDIReceiver mMIDIReceiver;
В Synthesis.cpp напишите такую простую функцию:
void Synthesis::ProcessMidiMsg(IMidiMsg* pMsg) {
mMIDIReceiver.onMessageReceived(pMsg);
}
Она вызывается каждый раз при получении MIDI сообщения, и мы передаем сообщение нашему получателю.
Давайте теперь немного наведем порядок. Отредактируйте оба enums
наверху:
enum EParams
{
kNumParams
};
enum ELayout
{
kWidth = GUI_WIDTH,
kHeight = GUI_HEIGHT
};
И создайте только один пресет по умолчанию:
void Synthesis::CreatePresets() {
MakeDefaultPreset((char *) "-", kNumPrograms);
}
При изменении параметров плагина ничего делать не надо:
void Synthesis::OnParamChange(int paramIdx)
{
IMutexLock lock(this);
}
Ручка в интерфейсе нам уже не пригодится. Давайте сократим конструктор до минимально необходимого:
Synthesis::Synthesis(IPlugInstanceInfo instanceInfo)
: IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo) {
TRACE;
IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight);
pGraphics->AttachPanelBackground(&COLOR_RED);
AttachGraphics(pGraphics);
CreatePresets();
}
Когда меняются настройки звука в системе, нам надо сообщить осциллятору новую частоту семплирования:
void Synthesis::Reset()
{
TRACE;
IMutexLock lock(this);
mOscillator.setSampleRate(GetSampleRate());
}
У нас еще осталась функция ProcessDoubleReplacing
. Задумайтесь: mMIDIReceiver.advance()
нужно выполнять каждый семпл. После этого мы узнаем частоту и громкость при помощи getLastVelocity
и getLastFrequency
у получателя MIDI. Затем вызываем mOscillator.setFrequency()
и mOscillator.generate()
для заполнения аудиобуфера звуком нужной частоты.
Функция generate
создавалась для обработки целого буфера; получатель MIDI же работает на уровне отдельного семпла: сообщения могут иметь любое смещение в пределах буфера, а значит mLastFrequency
может измениться на любом семпле. Придется доработать класс Oscillator
, чтобы он тоже работал на уровне семплов.
Сначала вынесем twoPI
из generate
и переместим в private
секцию Oscillator.h. Пока мы тут, давайте сразу добавим переменную липа bool
для обозначения того, заглушен ли осциллятор (т.е. ни одна нота не играется):
const double twoPI;
bool isMuted;
Инициализируем их путем добавления в список инициализации конструктора. Так это выглядит:
Oscillator() :
mOscillatorMode(OSCILLATOR_MODE_SINE),
mPI(2*acos(0.0)),
twoPI(2 * mPI), // This line is new
isMuted(true), // And this line
mFrequency(440.0),
mPhase(0.0),
mSampleRate(44100.0) { updateIncrement(); };
Добавим сеттер inline в секцию public:
inline void setMuted(bool muted) { isMuted = muted; }
И сразу под этой вставьте следующую строку:
double nextSample();
Будем вызывать эту функцию каждый семпл и получать от осцилятора аудиоданные.
Добавьте следующий код в Oscilator.cpp:
double Oscillator::nextSample() {
double value = 0.0;
if(isMuted) return value;
switch (mOscillatorMode) {
case OSCILLATOR_MODE_SINE:
value = sin(mPhase);
break;
case OSCILLATOR_MODE_SAW:
value = 1.0 - (2.0 * mPhase / twoPI);
break;
case OSCILLATOR_MODE_SQUARE:
if (mPhase <= mPI) {
value = 1.0;
} else {
value = -1.0;
}
break;
case OSCILLATOR_MODE_TRIANGLE:
value = -1.0 + (2.0 * mPhase / twoPI);
value = 2.0 * (fabs(value) - 0.5);
break;
}
mPhase += mPhaseIncrement;
while (mPhase >= twoPI) {
mPhase -= twoPI;
}
return value;
}
Как видим, тут используется twoPI
. Было бы избыточным вычислять это значение каждый семпл, поэтому мы и добавили два пи как константу в класс.
Когда осциллятор ничего не генерирует, мы возвращаем ноль. Конструкция switch
вам уже знакома, хоть тут мы и не используем цикл for
. Здесь мы просто генерируем одно значение для буфера, вместо того, чтобы заполнять его целиком. Также подобная структура позволяет нам вынести в конец приращение фазы, избегая повторений.
Это был неплохой пример рефакторинга, причиной которого послужила недостаточная гибкость кода. Конечно, мы могли бы подумать часок-другой прежде чем начинать писать функцию generate
с «буферным» подходом. Но эта реализация заняла у нас меньше часа целиком. В простых приложениях (типа этого) иногда эффективней реализовать какой-нибудь подход и посмотреть, как код справляется с задачей на практике. Чаще всего, как мы только что увидели, оказывается, что в целом идея была правильной (принцип вычисления разных звуковых волн), но какой-то аспект проблемы был упущен. С другой стороны, если вы разрабатываете публичный API, то менять что-то позже, мягко говоря, неудобно, так что тут надо все продумать заранее. В общем, это зависит от ситуации.
Функция setFrequency
тоже будет вызываться каждый семпл. Значит, updateIncrement
тоже будет вызываться очень часто. Но она пока что не оптимизирована:
void Oscillator::updateIncrement() {
mPhaseIncrement = mFrequency * 2 * mPI / mSampleRate;
}
2 * mPI * mSampleRate
меняется только при изменении частоты дискретизации. Так что результат этого вычисления лучше запомнить и пересчитывать его только внутри Oscillator::setSampleRate
. Стоить помнить, что запредельная оптимизация может сделать код нечитабельным и даже уродливым. В конкретном случае у нас не возникнет проблем с производительностью, т. к. мы пишем элементарный монофонический синт. Когда доберемся до полифонии, это будет другое дело, и тогда уже обязательно оптимизируем.
Теперь мы можем переписать ProcessDoubleReplacing
в Synthesis.cpp:
void Synthesis::ProcessDoubleReplacing(
double** inputs,
double** outputs,
int nFrames)
{
// Mutex is already locked for us.
double *leftOutput = outputs[0];
double *rightOutput = outputs[1];
for (int i = 0; i < nFrames; ++i) {
mMIDIReceiver.advance();
int velocity = mMIDIReceiver.getLastVelocity();
if (velocity > 0) {
mOscillator.setFrequency(mMIDIReceiver.getLastFrequency());
mOscillator.setMuted(false);
} else {
mOscillator.setMuted(true);
}
leftOutput[i] = rightOutput[i] = mOscillator.nextSample() * velocity / 127.0;
}
mMIDIReceiver.Flush(nFrames);
}
В цикле for получатель MIDI cначала обновляет значения (вызывается advance
). Если звучит нота (velocity > 0
), мы обновляем частоту осциллятора и даем ему звучать. В противном случае мы заглушаем его (тогда nextSample
будет возвращать нули).
Далее все сводится просто к вызову nextSample
для получения значения, изменения громкости (velocity
это целое число между 0
и 127
) и записи результата в выходные буферы. В конце вызывается Flush
для удаления начала очереди.
Запускайте VST или AU. Если AU не появляется в хосте, то, возможно, придется изменить PLUG_UNIQUE_ID
в resource.h. Если у двух плагинов одинаковый ID, хост будет игнорировать все, кроме какого-то одного.
Плагину надо подать на вход какие-нибудь MIDI данные. Проще всего использовать виртуальную клавиатуру REAPERа (меню View → Virtual MIDI Keyboard). На треке с плагином слева есть круглая красная кнопка. Зайдите в конфигурацию MIDI правым кликом по ней и выберите получение сообщений от виртуальной клавиатуры:
В том же меню включите Monitor Input. Теперь, когда фокус на окне виртуальной клавиатуры, можно играть на синтезаторе обычной клавиатурой. Наберите ваше имя или пароль от менеджера пароля и послушайте, как это звучит.
Если же у вас есть MIDI клавиатура, то, подключив ее, можно потестировать и самостоятельное приложение. Главное выбрать правильный MIDI вход. Если не слышно никакого звука, попробуйте удалить ~/Library/Application Support/Synthesis/settings.ini.
Весь проект на данной стадии можно скачать отсюда [12].
В следующий раз мы добавим симпатичную клавиатуру в интерфейс :)
Автор: 1eqinfinity
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/62597
Ссылки в тексте:
[1] Часть 1. Введение и настройка: http://habrahabr.ru/post/224911/
[2] Часть 2. Изучение кода: http://habrahabr.ru/post/225019/
[3] Часть 3. VST и AU: http://habrahabr.ru/post/225457/
[4] Часть 4. Цифровой дисторшн: http://habrahabr.ru/post/225751/
[5] Часть 5. Пресеты и GUI: http://habrahabr.ru/post/225755/
[6] Часть 6. Синтез сигналов: http://habrahabr.ru/post/226439/
[7] Часть 7. Получение MIDI сообщений: http://habrahabr.ru/post/226573/
[8] Control Changes: http://en.wikipedia.org/wiki/General_MIDI#Controller_events
[9] IMidiQueue: https://github.com/olilarkin/wdl-ol/blob/master/WDL/IPlug/IMidiQueue.h
[10] приоритет последней ноты: http://www.soundonsound.com/sos/oct00/articles/synthsec.htm
[11] не будет изменять неизменяемые члены своего класса: http://stackoverflow.com/a/5598730/966567
[12] отсюда: http://martin-finke.de/blog/articles/audio-plugins-009-receiving-midi/source.zip
Нажмите здесь для печати.