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

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

 
Сьогодні ми зробимо резонансний фільтр. Розробка фільтрів — це складна область, над якою ламають голову безліч DSP інженерів по всьому світу. Ми не будемо занурюватися в її нетрі, а створимо простий фільтр нижніх частот (Low-Pass ), смугової (Band-Pass ) і фільтр високих частот (High-Pass ) на основі алгоритму Пола Келлетом.
 
 
Почнемо, як ви здогадуєтеся, з створення класу
Filter
. Видаліть
#include <iostream>
з Filter.h (якщо є) і вставте таке оголошення класу:
 
 
class Filter {
public:
    enum FilterMode {
        FILTER_MODE_LOWPASS = 0,
        FILTER_MODE_HIGHPASS,
        FILTER_MODE_BANDPASS,
        kNumFilterModes
    };
    Filter() :
    cutoff(0.99),
    resonance(0.0),
    mode(FILTER_MODE_LOWPASS),
    buf0(0.0),
    buf1(0.0)
    {
        calculateFeedbackAmount();
    };
    double process(double inputValue);
    inline void setCutoff(double newCutoff) { cutoff = newCutoff; calculateFeedbackAmount(); };
    inline void setResonance(double newResonance) { resonance = newResonance; calculateFeedbackAmount(); };
    inline void setFilterMode(FilterMode newMode) { mode = newMode; }
private:
    double cutoff;
    double resonance;
    FilterMode mode;
    double feedbackAmount;
    inline void calculateFeedbackAmount() { feedbackAmount = resonance + resonance/(1.0 - cutoff); }
    double buf0;
    double buf1;
};

 
У
private
знаходяться такі параметри як частота зрізу
cutoff
і резонанс
resonance
.
Mode
визначає поточний режим роботи фільтра (Lowpass, Highpass, Bandpass). Змінні
feedbackAmount
,
buf0
і
buf1
використовуються для алгоритму фільтрації, але про це пізніше.
Конструктор просто ініціалізує змінні і обчислює рівень зворотного зв'язку (
calculateFeedbackAmount()
). Функція
process
викликається кожного семпл для обробки сигналу. Так як
feedbackAmount
залежить від
cutoff
і
resonance
, сеттери повинні викликати
calculateFeedbackAmount
кожен раз після оновлення цих параметрів.
 
Додайте в Filter.cpp алгоритм фільтра:
 
 
// By Paul Kellett
// http://www.musicdsp.org/showone.php?id=29

double Filter::process(double inputValue) {
    buf0 += cutoff * (inputValue - buf0);
    buf1 += cutoff * (buf0 - buf1);
    switch (mode) {
        case FILTER_MODE_LOWPASS:
            return buf1;
        case FILTER_MODE_HIGHPASS:
            return inputValue - buf0;
        case FILTER_MODE_BANDPASS:
            return buf0 - buf1;
        default:
            return 0.0;
    }
}

 
Короткий, да? По суті, він представляє з себе послідовно підключені НЧ фільтри першого порядку . «Першого порядку» грубо кажучи означає, що амплітуда складових сигналу над частотою зрізу кожну октаву стає в два рази нижче (тобто гучність падає на 6 дБ). Два рядки з обчисленнями
buf0
і
buf1
ідентичні: це і є два НЧ фільтра першого порядку. Перша на вхід приймає
inputValue
, друга —
buf0
(вихід першого фільтра). Просте послідовне з'єднання. Два рази по 6 дБ / окт дає нам 12 дБ / окт. У
switch
видно, що
buf1
це вихід НЧ фільтра. Якщо, наприклад, замість нього повертати
buf0
, то звук буде падати не на 12, а на 6 дБ / окт, тобто в сигналі буде більше високих частот.
 
З
case
FILTER_MODE_HIGHPASS
все зрозуміло по назві.
buf0
це тільки низькочастотна складова фільтра першого порядку. Якщо з
inputValue
відняти
buf0
, залишаться високочастотні складові. Можна відняти і
buf1
, щоб зріз був більш крутим.
 
З
case
FILTER_MODE_BANDPASS
ясно, що
buf0 - buf1
це смуговий фільтр. У
buf0
міститься трохи більше компонент над частотою зрізу, ніж їх міститься в
buf1
. Віднімаючи, ми отримуємо різницю.
Висновок наступний: віднімаючи низькочастотний вихід вищого порядку з низькочастотного виходу нижчого порядку , отримаємо смуговий фільтр.
 
Обчислення
buf1
залежить від свого попереднього значення. Така структура зі зворотним зв'язком називається фільтром з нескінченною імпульсною характеристикою (Infinite Impulse Response , скорочено IIR ). Прочитайте цей матеріал , щоб трохи краще розібратися з типами фільтрів. Заглиблюватися в це зараз не варто, так як математика в цій області не найпростіша, і це не мета даного керівництва.
 
 Ліричний відступ
 
Фільтри є одними з тих компонентів синтезатора, які визначають не тільки характер, а й об'єктивне якість звучання. Кращі реалізації фільтрів, як правило, відносно затратні з точки зору обчислень, а отже, розробникам доводиться вибирати між якістю окремих компонентів і загальної функціональністю. Саме тому в таких чудових по гнучкості і різноманітності ефектів інструментах, як NI Massive, не можна домогтися «теплого лампового звуку ». Його звук може бути навороченим і цікавим, але як би ви не намагалися зімітувати просте класичне звучання, звук буде тьмяний. Але в даному випадку лампове тепло не потрібно, адже фокус уваги слухача зміщений на динаміку компонентів звуку (синтезатори типу Massive є найпопулярнішими інструментами для створення дабстепа). У той же час такий «простий» інструмент, як, наприклад, AAS Ultra Analog, не дивлячись на відсутність безлічі різних огинають, низькочастотних осциляторів і інших ефектів, має естетично приємний звук, кожну ноту якого можна слухати як живий інструмент. Якщо ви хочете більше заглибитися в тему розробки фільтрів, є хороша, лаконічна і безкоштовна книга , написана Вадимом Завалішиним, багато років працюють над проектом NI Reaktor. Звичайно, у дизайні інструментів застосовуються і інші хитрощі крім якісної імплементації фільтрів — тремтіння фази і амплітуди, спектральне насичення тощо Але повернемося до нашого плагіну.
 
 Використання фільтра
 
Давайте включати фільтр в синтезатор. Почнемо з GUI. Видаліть bg.png з проекту ("Move to trash" в Xcode), скачайте і закиньте в проект нові зображення:
 
 
     
filtermode.png
 knob_small.png (Та ж ручка, але розміром 50 на50 пікселів)
 bg.png (Новий фон з простором під елементи управління фільтром)
 
 
Дописуємо посилання і ID в resource.h :
 
 
// 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 KNOB_SMALL_ID 106
#define FILTERMODE_ID 107

// 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 KNOB_SMALL_FN "resources/img/knob_small.png"
#define FILTERMODE_FN "resources/img/filtermode.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
WAVEFORM_ID       PNG WAVEFORM_FN
KNOB_ID       PNG KNOB_FN
KNOB_SMALL_ID       PNG KNOB_SMALL_FN
FILTERMODE_ID       PNG FILTERMODE_FN

 
Не забудьте
#include "Filter.h"
в Synthesis.h і додайте в
private
:
 
 
Filter mFilter;

 
Доповнимо
EParams
в Synthesis.cpp :
 
 
enum EParams
{
    mWaveform = 0,
    mAttack,
    mDecay,
    mSustain,
    mRelease,
    mFilterMode,
    mFilterCutoff,
    mFilterResonance,
    mFilterAttack,
    mFilterDecay,
    mFilterSustain,
    mFilterRelease,
    mFilterEnvelopeAmount,
    kNumParams
};

 
Змініть в конструкторі вертикальне положення перемикача форм хвилі:
 
 
pGraphics->AttachControl(new ISwitchControl(this, 24, 38, mWaveform, &waveformBitmap));

 
Ручки обвідної можна залишити як є. Потрібно додати перемикач режимів фільтра (Lowpass, Highpass, Bandpass). Додайте його перед
AttachGraphics(pGraphics)
:
 
 
GetParam(mFilterMode)->InitEnum("Filter Mode", Filter::FILTER_MODE_LOWPASS, Filter::kNumFilterModes);
IBitmap filtermodeBitmap = pGraphics->LoadIBitmap(FILTERMODE_ID, FILTERMODE_FN, 3);
pGraphics->AttachControl(new ISwitchControl(this, 24, 123, mFilterMode, &filtermodeBitmap));

 
Нам знадобляться ручки для зміни частоти зрізу і резонансу. Додайте їх туди ж. Будемо використовувати нову ручку knob_small.png :
 
 
// Knobs for filter cutoff and resonance
IBitmap smallKnobBitmap = pGraphics->LoadIBitmap(KNOB_SMALL_ID, KNOB_SMALL_FN, 64);
// Cutoff knob:
GetParam(mFilterCutoff)->InitDouble("Cutoff", 0.99, 0.01, 0.99, 0.001);
GetParam(mFilterCutoff)->SetShape(2);
pGraphics->AttachControl(new IKnobMultiControl(this, 5, 177, mFilterCutoff, &smallKnobBitmap));
// Resonance knob:
GetParam(mFilterResonance)->InitDouble("Resonance", 0.01, 0.01, 1.0, 0.001);
pGraphics->AttachControl(new IKnobMultiControl(this, 61, 177, mFilterResonance, &smallKnobBitmap));

 
Тримаєте в думці, що значення зрізу не в якому разі не повинно бути
1.0
, інакше всередині
calculateFeedbackAmount
все поділиться на нуль, буквально.
У
ProcessDoubleReplacing
згодуйте згенеровані семпли звуку фільтру:
 
 
leftOutput[i] = rightOutput[i] = mFilter.process(mOscillator.nextSample() * mEnvelopeGenerator.nextSample() * velocity / 127.0);

 
Фільтр повинен реагувати на зміну стадій обвідної. Доповніть
switch
у функції
Synthesis::OnParamChange
:
 
 
case mFilterCutoff:
    mFilter.setCutoff(GetParam(paramIdx)->Value());
    break;
case mFilterResonance:
    mFilter.setResonance(GetParam(paramIdx)->Value());
    break;
case mFilterMode:
    mFilter.setFilterMode(static_cast<Filter::FilterMode>(GetParam(paramIdx)->Int()));
    break;

 
Можна провести перші випробування. Зобразіть небудь і покрутіть ручку в процесі гри.
 
 Резонанс
 
Резонанс — це просто пік на частоті зрізу. Його можна створити, взявши смугову складову, помноживши її на деяке значення і перебував до вихідного сигналу.
Змініть перший рядок в алгоритмі фільтра:
 
 
buf0 += cutoff * (inputValue - buf0 + feedbackAmount * (buf0 - buf1));

 
Все, що ми зробили, це додали вихід полосного фільтра (
buf0 – buf1
), помножений на
feedbackAmount
. У тілі функції
calculateFeedbackAmount
параметр
feedbackAmount
пропорційний
resonance
, тобто пік буде тим голосніше, чим вище резонанс.
 
Запустіть плагін, затисніть будь-яку ноту і покрутіть ручку частоти зрізу з різними значеннями резонансу. Якщо викрутити резонанс на максимум, виникне автоколиваннями , яке можна використовувати для створення цікавих ефектів . Маючи всього декілька параметрів, ми вже можемо витягувати цілу палітру різноманітних звуків, особливо з ВЧ і смуговим фільтром.
 
 Від -12 дБ / окт до -24 дБ / окт
 
Замість двох давайте запив чотири фільтра в ряд! Це дасть нам загасання 24 децибели на октаву. Додайте пару рядків перед
switch
в
Filter::process
:
 
 
buf2 += cutoff * (buf1 - buf2);
buf3 += cutoff * (buf2 – buf3);

 
Ідея та ж: на вхід приймаємо вихід попереднього фільтра, віднімаємо з нього попереднє значення поточного фільтра, множимо цю різницю на зріз і додаємо до попереднього значення поточного фільтра. Це можна продовжувати і далі, але фільтр стане витратним за обчисленнями і, можливо, нестабільним (обговоримо це докладніше в одному з наступних постів). Не забудьте змінити і
switch
:
 
 
switch (mode) {
    case FILTER_MODE_LOWPASS:
        return buf3;
    case FILTER_MODE_HIGHPASS:
        return inputValue - buf3;
    case FILTER_MODE_BANDPASS:
        return buf0 - buf3;
    default:
        return 0.0;
}

 
Тут замість
buf1
використовується
buf3
— вихід чотирьох послідовних фільтрів першого порядку. Загасання компонент одно -24 дБ / окт. Ми ще не оголосили
buf2
і
buf3
, так що давайте зробимо це в секції
private
Filter.h :
 
 
double buf2;
double buf3;

 
І инициализируем їх нулями, як і
buf0
c
buf1
:
 
 
Filter() :
    // ...
    buf0(0.0),
    buf1(0.0),
    buf2(0.0),
    buf3(0.0)
    // ...

 
Якщо запустити плагін і послухати, помітно, що фільтр став різати крутіше: частоти над зрізом заглушені сильніше. Це дещо ближче до звучання аналогових синтезаторів.
А що якщо ми будемо змінювати частоту зрізу в часі?
 
 Огинаюча фільтра
 
Найцікавіше ще попереду :) Завдяки тому, що ми з самого початку намагалися робити наш дизайн добре структурованим, додати другу огибающую для фільтра дуже просто.
Взагалі-то нам не варто безпосередньо змінювати
cutoff
за допомогою обвідної. Частота зрізу пов'язана з ручкою інтерфейсу. Давайте додамо змінну
cutoffMod
, яка буде змінюватися обвідної, і яку ми будемо додавати до
cutoff
для обчислення сумарного зрізу. У хедері фільтра додайте
#include <cmath>
і допишіть в
private
змінну:
 
 
double cutoffMod;

 
Ініціалізується:
 
 
Filter() :
    cutoff(0.99),
    resonance(0.01),
    cutoffMod(0.0),
    // ...

 
Сумарний зріз не повинен виходити з області допустимих значень. Додайте в
private
рядки:
 
 
inline double getCalculatedCutoff() const {
    return fmax(fmin(cutoff + cutoffMod, 0.99), 0.01);
};

 
 
calculateFeedbackAmount
повинна використовувати цей обчислений зріз:
 
 
inline void calculateFeedbackAmount() {
    feedbackAmount = resonance + resonance/(1.0 - getCalculatedCutoff());
}

 
І давайте додамо публічний сетер для
cutoffMod
. Так як
feedbackAmount
залежить від обчисленого зрізу, сетер і його повинен оновлювати:
 
 
inline void setCutoffMod(double newCutoffMod) {
    cutoffMod = newCutoffMod;
    calculateFeedbackAmount();
}

 
Логічно, що й алгоритм фільтра треба трохи підправити. Перші рядки
Filter::process
повинні виглядати так:
 
 
if (inputValue == 0.0) return inputValue;
double calculatedCutoff = getCalculatedCutoff();
buf0 += calculatedCutoff * (inputValue - buf0 + feedbackAmount * (buf0 - buf1));
buf1 += calculatedCutoff * (buf0 - buf1);
buf2 += calculatedCutoff * (buf1 - buf2);
buf3 += calculatedCutoff * (buf2 – buf3);

 
Завдяки першому рядку фільтр не буде працювати даремно, коли його вхід мовчить. Подібна перевірка має сенс, коли обумовлений нею код помітно складніше, ніж сама перевірка. Це якраз той випадок. Крім цього і заміни
cutoff
на
calculatedCutoff
все залишається як і раніше.
 
Тепер, коли зрізом фільтра можна управляти ззовні (викликаючи
setCutoffMod
), в клас
Synthesis
треба додати огибающую фільтра, яка буде запускатися так само, як існуюча огинає осцилятора. Користувач зможе міняти те, наскільки сильно ця огинає впливатиме на
cutoffMod
. Додамо новий параметр
filterEnvelopeAmount
зі значеннями від
-1
до
+1
. Потім допрацюємо GUI.
 
У секцію
private
Synthesis.h додайте пару членів класу:
 
 
EnvelopeGenerator mFilterEnvelopeGenerator;
double filterEnvelopeAmount;

 
Ми хочемо запускати обидва генератора огинають MIDI повідомленнями, так що треба відредагувати функції
onNoteOn
і
onNoteOff
:
 
 
inline void onNoteOn(const int noteNumber, const int velocity) {
    mEnvelopeGenerator.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK);
    mFilterEnvelopeGenerator.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK);
};
inline void onNoteOff(const int noteNumber, const int velocity) {
    mEnvelopeGenerator.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE);
    mFilterEnvelopeGenerator.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE);
};

 
Сенс залишився той же. У тілі
ProcessDoubleReplacing
безпосередньо перед обчисленням семплів звуку додайте цей рядок:
 
 
mFilter.setCutoffMod(mFilterEnvelopeGenerator.nextSample() * filterEnvelopeAmount);

 
Як бачите, ми перемножуємо таке значення семпла обвідної фільтра
nextSample
на
filterEnvelopeAmount
і записуємо результат в
cutoffMod
. Треба не забути ініціалізувати
filterEnvelopeAmount
в конструкторі:
 
 
Synthesis::Synthesis(IPlugInstanceInfo instanceInfo) : IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo),
    lastVirtualKeyboardNoteNumber(virtualKeyboardMinimumNoteNumber - 1),
    filterEnvelopeAmount(0.0) {
    // ...
}

 
І обов'язково встановити частоту семплювання в
Synthesis::Reset
:
 
 
mFilterEnvelopeGenerator.setSampleRate(GetSampleRate());

 
Ми вже додали параметри в
EParams
, тепер їх треба ініціалізувати і додати ручки. Перед
AttachGraphics
в конструкторі додайте все це:
 
 
// Knobs for filter envelope
// Attack knob
GetParam(mFilterAttack)->InitDouble("Filter Env Attack", 0.01, 0.01, 10.0, 0.001);
GetParam(mFilterAttack)->SetShape(3);
pGraphics->AttachControl(new IKnobMultiControl(this, 139, 178, mFilterAttack, &smallKnobBitmap));
// Decay knob:
GetParam(mFilterDecay)->InitDouble("Filter Env Decay", 0.5, 0.01, 15.0, 0.001);
GetParam(mFilterDecay)->SetShape(3);
pGraphics->AttachControl(new IKnobMultiControl(this, 195, 178, mFilterDecay, &smallKnobBitmap));
// Sustain knob:
GetParam(mFilterSustain)->InitDouble("Filter Env Sustain", 0.1, 0.001, 1.0, 0.001);
GetParam(mFilterSustain)->SetShape(2);
pGraphics->AttachControl(new IKnobMultiControl(this, 251, 178, mFilterSustain, &smallKnobBitmap));
// Release knob:
GetParam(mFilterRelease)->InitDouble("Filter Env Release", 1.0, 0.001, 15.0, 0.001);
GetParam(mFilterRelease)->SetShape(3);
pGraphics->AttachControl(new IKnobMultiControl(this, 307, 178, mFilterRelease, &smallKnobBitmap));

// Filter envelope amount knob:
GetParam(mFilterEnvelopeAmount)->InitDouble("Filter Env Amount", 0.0, -1.0, 1.0, 0.001);
pGraphics->AttachControl(new IKnobMultiControl(this, 363, 178, mFilterEnvelopeAmount, &smallKnobBitmap));

 
Всі практично те ж саме, але тут замість великої ручки використовуємо
smallKnobBitmap
. Вдобавок до чотирьох ручкам для стадій обвідної фільтра ми зробили ще одну для регулювання кількості впливу обвідної на фільтр. Залишилося тільки реагувати на те, як користувач крутить ручки. У
Synthesis::OnParamChange
додайте в існуючий
switch
:
 
 
case mFilterAttack:
    mFilterEnvelopeGenerator.setStageValue(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK, GetParam(paramIdx)->Value());
    break;
case mFilterDecay:
    mFilterEnvelopeGenerator.setStageValue(EnvelopeGenerator::ENVELOPE_STAGE_DECAY, GetParam(paramIdx)->Value());
    break;
case mFilterSustain:
    mFilterEnvelopeGenerator.setStageValue(EnvelopeGenerator::ENVELOPE_STAGE_SUSTAIN, GetParam(paramIdx)->Value());
    break;
case mFilterRelease:
    mFilterEnvelopeGenerator.setStageValue(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE, GetParam(paramIdx)->Value());
    break;
case mFilterEnvelopeAmount:
    filterEnvelopeAmount = GetParam(paramIdx)->Value();
    break;

 
 Кислота!
 
Запускайте і тестуйте нову функціональність! Спробуйте ось такі положення ручок і пограйте на нижніх нотах (де-небудь в басейні C1), буде забавно хлюпати:
 
 
 
З незначними доробками ми використовували наш клас
EnvelopeGenerator
для фільтра. Це робить синтезатор більш гнучким і розширює палітру звуків. Ми вже наближаємося до завершення класичного монофонічного синтезатора!
 
Ісходникі можна скачати звідси .
 
Наступного разу ми додамо низькочастотний осцилятор :)
    
Джерело: Хабрахабр

0 коментарів

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