Створення аудіоплагіна, частина 15

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

 
У цьому пості ми закінчимо роботу над поліфонією: Причешу код і наведемо GUI в робочий стан.
 
 
 
 Суботник
 
Почнемо з
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
. Всі інші частини коду залишаться як є.
 
 Готово!
 
Вітаю, ви написали свій власний поліфонічний синтезатор! Я знаю, що роботи було багато, але сподіваюся, що це не було захмарно складно. Це дійсно великий проект.
 
Код і всі зображення можна завантажити звідси .
 
В якості вишеньки на торті, в наступному пості ми позбудемося аліасинга в осцилляторах.
 
 Оригінал поста .
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.