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

в 15:20, , рубрики: c++, dsp, Martin Finke, Oli Larkin, Visual Studio, VST, xcode, интерфейсы, перевод, Работа со звуком

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


В этом посте мы закончим работу над полифонией: причешем код и приведем GUI в рабочее состояние.

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

Субботник

Начнем с MIDIReceiver. Так как структура теперь полифоническая, нам не нужны переменные mLast. Из MIDIReceiver.h удалите mLastNoteNumber, mLastFrequency и mLastVelocity, включая их инициализации и геттеры, т.е. getLastNoteNumber, getLastFrequency и getLastVelocity. Также удалите noteNumberToFrequency. На всякий случай, вот так должен выглядеть класс теперь:

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

public:
    MIDIReceiver() :
    mNumKeys(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; }
    void advance();
    void onMessageReceived(IMidiMsg* midiMessage);
    inline void Flush(int nFrames) { mMidiQueue.Flush(nFrames); mOffset = 0; }
    inline void Resize(int blockSize) { mMidiQueue.Resize(blockSize); }

    Signal2< int, int > noteOn;
    Signal2< int, int > noteOff;
};

Измените функцию advance в MIDIReceiver.cpp:

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;
                noteOn(noteNumber, velocity);
            }
        } else {
            if(mKeyStatus[noteNumber] == true) {
                mKeyStatus[noteNumber] = false;
                mNumKeys -= 1;
                noteOff(noteNumber, velocity);
            }
        }
        mMidiQueue.Remove();
    }
    mOffset++;
}

Здесь поменялись три вещи:

  • Удалены члены mLast
  • Сигнал noteOn генерируется при нажатии любой клавиши (номер ноты не должен отличаться от mLastNoteNumber)
  • Сигнал noteOff тоже генерируется при отпускании любой клавиши (номер ноты не должен быть равен mLastNoteNumber)

Переходим к SpaceBass.h. Тут нужно удалить следующее:

  • #include для Oscillator.h, EnvelopeGenerator.h и Filter.h, вместо них написать #include "VoiceManager.h"
  • mOscillator, mEnvelopeGenerator, mFilter, mFilterEnvelopeGenerator и mLFO
  • filterEnvelopeAmount и lfoFilterModAmount
  • функции-члены onNoteOn, onNoteOff, onBeginEnvelopeCycle, onFinishedEnvelopeCycle

Код выглядит получше, да? Класс плагина больше не взаимодействует напрямую с другими классами, только с VoiceManager.
Добавьте в секцию private:

VoiceManager voiceManager;

Измените конструктор класса в SpaceBass.cpp:

SpaceBass::SpaceBass(IPlugInstanceInfo instanceInfo) : IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo), lastVirtualKeyboardNoteNumber(virtualKeyboardMinimumNoteNumber - 1) {
    TRACE;

    CreateParams();
    CreateGraphics();
    CreatePresets();

    mMIDIReceiver.noteOn.Connect(&voiceManager, &VoiceManager::onNoteOn);
    mMIDIReceiver.noteOff.Connect(&voiceManager, &VoiceManager::onNoteOff);
}

mMIDIReceiver теперь подключен к VoiceManager, а не к классу плагина. EnvelopeGenerator теперь управляются классами VoiceManager и Voice, так что мы больше не подцепляем их при помощи Connect().
Теперь компилятор ругается, т.к. ProcessDoubleReplacing обращается к тем вещам, которые мы только что удалили. Новая имплементация очень простая: это вызов VoiceManager::nextSample:

void SpaceBass::ProcessDoubleReplacing(
    double** inputs,
    double** outputs,
    int nFrames)
{
    // Mutex is already locked for us.

    double *leftOutput = outputs[0];
    double *rightOutput = outputs[1];
    processVirtualKeyboard();
    for (int i = 0; i < nFrames; ++i) {
        mMIDIReceiver.advance();
        leftOutput[i] = rightOutput[i] = voiceManager.nextSample();
    }

    mMIDIReceiver.Flush(nFrames);
}

Обратите внимание, что все взаимодействия с осцилляторами, генераторами огибающих и фильтрами исчезли.
В теле Reset() у нас больше нет доступа к этим компонентам, так что нужно сделать так, чтобы VoiceManager менял частоту дискретизации для всех компонентов:

void SpaceBass::Reset()
{
    TRACE;
    IMutexLock lock(this);
    double sampleRate = GetSampleRate();
    voiceManager.setSampleRate(sampleRate);
}

setSampleRate еще не имплементирована, допишите ее в секцию public в хедере VoiceManager.h:

void setSampleRate(double sampleRate) {
    EnvelopeGenerator::setSampleRate(sampleRate);
    for (int i = 0; i < NumberOfVoices; i++) {
        Voice& voice = voices[i];
        voice.mOscillatorOne.setSampleRate(sampleRate);
        voice.mOscillatorTwo.setSampleRate(sampleRate);
    }
    mLFO.setSampleRate(sampleRate);
}

По сути функция просто вызывает setSampleRate для каждого голоса и каждого компонента. Можно было бы статически вызвать Oscillator::mSampleRate, но все равно приходилось бы вызывать updateIncrement для обоих осцилляторов каждого голоса. Но на мой взгляд, первый вариант прозрачнее.

Теперь все прекрасно, кроме того, что ручки интерфейса не работают. Но перед тем, как мы их допилим, давайте добавим модуляцию тона для осцилляторов.

Модуляция тона

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

В функции Voice::nextSample сразу перед return допишите:

mOscillatorOne.setPitchMod(mLFOValue * mOscillatorOnePitchAmount);
mOscillatorTwo.setPitchMod(mLFOValue * mOscillatorTwoPitchAmount);

Как видите, величина модуляции тона определяется значением LFO, умноженным на значение параметра, связанного с ручкой GUI.
Добавьте в секцию private в Oscillator.h:

double mPitchMod;

И в список инициализации конструктора:

mPitchMod(0.0),

В секции public нужен сеттер:

void setPitchMod(double amount);

Саму функцию напишем в Oscillator.cpp:

void Oscillator::setPitchMod(double amount) {
    mPitchMod = amount;
    updateIncrement();
}

Устанавливая величину модуляции тона мы меняем саму воспроизводимую частоту, поэтому нам необходимо вызвать updateIncrement. А внутри этой функции нам надо как-то учесть mPitchMod. Давайте перепишем updateIncrement:

void Oscillator::updateIncrement() {
    double pitchModAsFrequency = pow(2.0, fabs(mPitchMod) * 14.0) - 1;
    if (mPitchMod < 0) {
        pitchModAsFrequency = -pitchModAsFrequency;
    }

Что тут происходит? mPitchMode меняется от -1 до 1, но нам было бы удобней представление типа «плюс 491.3 Гц». pow нам поможет с переводом в герцы, но потеряются отрицательные значения при вызове fabs (абсолютная величина). Следующий if вернет нам отрицательные значения. Вычитание единицы в конце нужно, потому что когда mPitchMod равен нулю, выражение pow(2.0, 0) даст 1, и мы получим модуляцию тона в 1 Гц, что неправильно.
Далее, мы вычисляем calculatedFrequency из основной частоты mFrequency и значение в герцах, которое мы только что получили:

 double calculatedFrequency = fmin(fmax(mFrequency + pitchModAsFrequency, 0), mSampleRate/2.0);

Сначала мы складываем эти две частоты. fmin гарантирует, что мы не превысим половину частоты дискретизации. Нам нельзя подниматься выше частоты Найквиста, иначе мы получим алиасинг. fmax гарантирует, что частота не упадет ниже нуля.
Ну и дописываем приращение фазы:

 mPhaseIncrement = calculatedFrequency * 2 * mPI / mSampleRate;
}

Тут все по-старому, кроме того, что теперь, естественно, мы используем calculatedFrequency.

Ручки интерфейса

Последняя часть головоломки! После этого наш синтюк будет готов!

Начнем с ручек LFO. Они не влияют на голоса, так что с ними все несколько иначе, нежели с другими параметрами. Замените функцию OnParamChange на новую:

void SpaceBass::OnParamChange(int paramIdx)
{
    IMutexLock lock(this);
    IParam* param = GetParam(paramIdx);
    if(paramIdx == mLFOWaveform) {
        voiceManager.setLFOMode(static_cast<Oscillator::OscillatorMode>(param->Int()));
    } else if(paramIdx == mLFOFrequency) {
        voiceManager.setLFOFrequency(param->Value());
    }
}

Мы проверяем, какой параметр изменяется, и вызываем либо setLFOMode, либо setLFOFrequency. Эти две функции еще не написаны, давайте это сделаем в секции public в VoiceManager.h:

inline void setLFOMode(Oscillator::OscillatorMode mode) { mLFO.setMode(mode); };
inline void setLFOFrequency(double frequency) { mLFO.setFrequency(frequency); };

Как видите, обе просто вызывают сеттеры mLFO.

Для остальных параметров мы используем функциональные механизмы С++. Это очень мощные и удобные вещи, но о них не так часто говорят в руководствах по языку. Я думаю, вы согласитесь с тем, что стоит о них знать. Итак, что за проблема возникает?

При повороте ручки вызывается OnParamChange с ID параметра и его значением. Скажем, mFilterCutoff со значением 0.3. Теперь мы сообщаем VoiceManager: «для каждого Voice установить срез фильтра 0.3». Далее, возможно, мы бы вызвали функцию setFilterCutoffForEachVoice, которая выглядела бы примерно так (привожу ее только для демонстрации):

VoiceManager::setFilterCutoffForEachVoice(double newCutoff) {
    for (int i = 0; i < NumberOfVoices; i++) {
        voice[i].mFilter.setCutoff(newCutoff);
    }
}

Вроде все неплохо, только нам понадобилось бы штук десять таких функций для разных параметров. Каждая была бы чуточку другой, но for во всех был бы одинаковым. Было бы хорошо иметь возможность сказать «Вот изменение, его надо применить ко всем голосам». В С++ такая возможность, естественно, есть. Можно взять функцию, заполнить ее предварительно некоторыми входными значениями, и вызывать ее для обработки разных вещей. Это похоже на Function.prototype.bind в JavaScript, только еще с проверкой на совместимость типов данных.
Давайте попробуем этот подход! Добавьте в VoiceManager.h:

#include <tr1/functional>
// #include <functional> if that doesn't work

В public добавьте:

typedef std::tr1::function<void (Voice&)> VoiceChangerFunction;

VoiceChangerFunction это функция, которая в качестве первого параметра берет Voice& и возвращает void. На самом деле вовсе не обязательно, чтобы это была именно функция. Подойдет все, что можно вызвать при помощи ().
После этого добавьте функцию:

inline void changeAllVoices(VoiceChangerFunction changer) {
    for (int i = 0; i < NumberOfVoices; i++) {
        changer(voices[i]);
    }
}

VoiceChangerFunction итерирует над всеми голосами и применяет к ним всем changer.
Под этим добавим сами функции. Все они выглядят примерно одинаково: каждая принимает ссылку на голос Voice& и некоторые другие параметры и изменяет этот голос.

// Functions to change a single voice:
static void setVolumeEnvelopeStageValue(Voice& voice, EnvelopeGenerator::EnvelopeStage stage, double value) {
    voice.mVolumeEnvelope.setStageValue(stage, value);
}
static void setFilterEnvelopeStageValue(Voice& voice, EnvelopeGenerator::EnvelopeStage stage, double value) {
    voice.mFilterEnvelope.setStageValue(stage, value);
}
static void setOscillatorMode(Voice& voice, int oscillatorNumber, Oscillator::OscillatorMode mode) {
    switch (oscillatorNumber) {
        case 1:
            voice.mOscillatorOne.setMode(mode);
            break;
        case 2:
            voice.mOscillatorTwo.setMode(mode);
            break;
    }
}
static void setOscillatorPitchMod(Voice& voice, int oscillatorNumber, double amount) {
    switch (oscillatorNumber) {
        case 1:
            voice.setOscillatorOnePitchAmount(amount);
            break;
        case 2:
            voice.setOscillatorTwoPitchAmount(amount);
            break;
    }
}
static void setOscillatorMix(Voice& voice, double value) {
    voice.setOscillatorMix(value);
}
static void setFilterCutoff(Voice& voice, double cutoff) {
    voice.mFilter.setCutoff(cutoff);
}
static void setFilterResonance(Voice& voice, double resonance) {
    voice.mFilter.setResonance(resonance);
}
static void setFilterMode(Voice& voice, Filter::FilterMode mode) {
    voice.mFilter.setFilterMode(mode);
}
static void setFilterEnvAmount(Voice& voice, double amount) {
    voice.setFilterEnvelopeAmount(amount);
}
static void setFilterLFOAmount(Voice& voice, double amount) {
    voice.setFilterLFOAmount(amount);
}

Но мы не можем передать их функции changeAllVoices. Они не являются функциями VoiceChangerFunction, потому что все принимают больше одного аргумента. Мы заполним предварительно все аргументы кроме первого (Voice&). А это уже превратит их в функции VoiceChangerFunction.
В SpaceBass.cpp добавьте #include <tr1/functional> (или #include <functional>). А в OnParamChange добавьте else в конце, должно выглядеть примерно так:

 // ...
    } else {
        using std::tr1::placeholders::_1;
        using std::tr1::bind;
        VoiceManager::VoiceChangerFunction changer;
        switch(paramIdx) {
            // We'll add this part in a moment
        }
        voiceManager.changeAllVoices(changer);
    }
}

Тут мы просто говорим, что не хотим печатать std::tr1:: каждый раз. Теперь можно просто писать _1 и bind (поясню через минутку). Также мы объявляем VoiceChangerFunction. switch применяет changer в зависимости от ID параметра. В конце мы вызываем changeAllVoices, передавая только что созданный changer.

Как же нам создать такой changer?

  • Возьмем одну из функций, определенных выше
  • При помощи std::tr1::bind заполним все аргументы кроме первого, Voice&

Это даст нам настоящую работающую функцию VoiceChangerFunction. На словах звучит сложновато, давайте лучше посмотрим, как это выглядит на деле. Добавьте следующий case в switch(paramIdx):

case mOsc1Waveform:
    changer = bind(&VoiceManager::setOscillatorMode,
                   _1,
                   1,
                   static_cast<Oscillator::OscillatorMode>(param->Int()));
    break;

Первый параметр для привязки (англ. bind) это функция, аргументы которой мы хотим предварительно заполнить. В данном случае setOscillatorMode. Остальные параметры — аргументы этой функции. _1 ставится на место того параметра, который не будет предварительно заполняться. Этот параметр будет передаваться при вызове changer. В нашем случае changer ожидает в качестве первого аргумента Voice&. Далее параметры проходят предварительное заполнение: форма волны номер 1 устанавливается для осциллятора. Нам необходимо привести целочисленный тип к виду OscillatorMode enum.

Теперь давайте создадим changer для всех остальных параметров. Принцип тот же. Если вы его усвоили, следующий код можете не печатать руками, а просто скопировать:

case mOsc1PitchMod:
    changer = bind(&VoiceManager::setOscillatorPitchMod, _1, 1, param->Value());
    break;
case mOsc2Waveform:
    changer = bind(&VoiceManager::setOscillatorMode, _1, 2, static_cast<Oscillator::OscillatorMode>(param->Int()));
    break;
case mOsc2PitchMod:
    changer = bind(&VoiceManager::setOscillatorPitchMod, _1, 2, param->Value());
    break;
case mOscMix:
    changer = bind(&VoiceManager::setOscillatorMix, _1, param->Value());
    break;
    // Filter Section:
case mFilterMode:
    changer = bind(&VoiceManager::setFilterMode, _1, static_cast<Filter::FilterMode>(param->Int()));
    break;
case mFilterCutoff:
    changer = bind(&VoiceManager::setFilterCutoff, _1, param->Value());
    break;
case mFilterResonance:
    changer = bind(&VoiceManager::setFilterResonance, _1, param->Value());
    break;
case mFilterLfoAmount:
    changer = bind(&VoiceManager::setFilterLFOAmount, _1, param->Value());
    break;
case mFilterEnvAmount:
    changer = bind(&VoiceManager::setFilterEnvAmount, _1, param->Value());
    break;
    // Volume Envelope:
case mVolumeEnvAttack:
    changer = bind(&VoiceManager::setVolumeEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_ATTACK, param->Value());
    break;
case mVolumeEnvDecay:
    changer = bind(&VoiceManager::setVolumeEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_DECAY, param->Value());
    break;
case mVolumeEnvSustain:
    changer = bind(&VoiceManager::setVolumeEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_SUSTAIN, param->Value());
    break;
case mVolumeEnvRelease:
    changer = bind(&VoiceManager::setVolumeEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_RELEASE, param->Value());
    break;
    // Filter Envelope:
case mFilterEnvAttack:
    changer = bind(&VoiceManager::setFilterEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_ATTACK, param->Value());
    break;
case mFilterEnvDecay:
    changer = bind(&VoiceManager::setFilterEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_DECAY, param->Value());
    break;
case mFilterEnvSustain:
    changer = bind(&VoiceManager::setFilterEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_SUSTAIN, param->Value());
    break;
case mFilterEnvRelease:
    changer = bind(&VoiceManager::setFilterEnvelopeStageValue, _1, EnvelopeGenerator::ENVELOPE_STAGE_RELEASE, param->Value());
    break;

Обратите внимание на проверку типов: у вас не получится заставить changer изменить частоту среза фильтра и заполнить этот параметр каким-нибудь enum или вообще неправильным числом параметров.
Если в какой-то момент вы захотите использовать все голоса вашего VoiceManager как связанный список с динамическим распределением памяти, надо будет изменить только changeAllVoices. Все остальные части кода останутся как есть.

Готово!

Поздравляю, вы написали свой собственный полифонический синтезатор! Я знаю, что работы было много, но надеюсь, что это не было заоблачно сложно. Это действительно большой проект.

Код и все изображения можно скачать отсюда.

В качестве вишенки на торте, в следующем посте мы избавимся от алиасинга в осцилляторах.

Оригинал поста.

Автор: 1eqinfinity

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js