Програмування&Музика: Delay, Distortion і модуляція параметрів. Частина 4

Всім привіт! Ви читаєте четверту частину статті про створення VST-синтезатора на З#. У попередніх частинах ми генерували сигнал, застосовували до нього амплітудну обвідна і фільтр частот.
В цей раз ми розглянемо ефекти Distortion — спотворення сигналу, знайоме кожному электрогитаристу і Delay (воно ж відлуння).
Безліч різних цікавих звучань можна отримати, якщо міняти (модулювати) значення параметрів складових частин синтезатора (генератора, фільтра, ефектів) у часі. Розглянемо варіант, як це можна зробити.
Вихідний код написаного мною синтезатора доступний на GitHub'е.

Скріншот VST плагін GClip

Цикл статей
  1. Розуміємо і пишемо VSTi синтезатор на C# WPF
  2. ADSR-обвідна сигналу
  3. Частотний фільтр Баттервота
  4. Delay, Distortion і модуляція параметрів
Зміст
  1. Кліпінг, спотворення, овердрайв і дісторшн
  2. Кодим ефект дісторшн
  3. Ділей і реверберація
  4. Кодим ефект ділею
  5. Модуляція параметрів
  6. Пишемо клас LFO
  7. Інтерфейс IParameterModifier і використання актуального значення параметра
  8. Висновок
  9. Список літератури

Кліпінг, спотворення, овердрайв і дісторшн
Початкові моделі гітарних підсилювачів і звукознімачів були простими і низькоякісними, відповідно, додавали спотворення в оброблюваний сигнал. При використанні аналогових підсилювачів сигнал спотворювався в залежності від вихідної гучності сигналу. Із зростанням амплітуди сигналу коефіцієнт нелінійних спотворень зростає, додаються різні гармоніки. Якщо ви включите ваші побутові колонки на максимум, то, впевнений, теж почуєте спотворення.
Є історія, як у 51'м році гітарист групи Kings of Rhythm використовував підсилювач, який був пошкоджений в дорозі, і продюсеру сподобалося звучання — таким чином, була зроблена одна з найбільш ранніх записів спотвореної гітари.
Ефект "Дісторшн" — перекладається з англійської як "спотворення". Якщо сигнал почати жорстко обмежувати по амплітуді, будуть створюватися нелінійні спотворення, з'являтися нові гармоніки. Чим більше обмеження (Theshold), тим більше спотворюється сигнал.
Майже будь-яка гітара в жанрі зі словом "рок" оброблена ефектом дісторшн або овердрайвом. Лінк на аудиопримеры знаменитих ефектів.
Овердрайв відрізняється більш плавним обмеженням амплітуди, ніж у дісторшна. Овердрайв ще називають Soft Clipping, а дісторшн, відповідно, Hard Clipping. Овердрайв на гітарах застосовують в більш "спокійних" жанрах типу інді-рок, поп-року і тому подібних.

Приблизне порівняння ефектів Distortion (Hard Clipping) і Overdrive (Soft Clipping)
Клиппингом називають небажані артефакти (клацання), при перевищенні цифровий амплітуди 0 dB. Є ефекти, що реалізує "чистий" (не емулюючи будь-які аналогові педальки або преампи) дісторшн сигналу. Наприклад, плагін GClip (на початку статті як раз його скрін) просто математично обрізає вхідний сигнал по амплітуді.

Кодим ефект дісторшн
З вищесказаного виводимо, що, по суті, жорсткий дісторшн визначається тільки параметром максимального абсолютного значення амплітуди — Threshold. У нас абсолютні значення семпла не перевищують 1, значить і Threshold укладено в інтервалі [0,1].
Чим більше ми обмежуємо сигнал (чим ближче Threshold до нуля), тим він стає слабкішим по гучності. Щоб гучність сигналу не змінювалася, можна її відновити: поділимо значення семпла на Threshold.
Отримуємо простий алгоритм для жорсткого дісторшна, який застосовується для кожного зразка окремо:

  1. Якщо значення семпла більше Threshold, зробити його рівним Threshold.
  2. Якщо значення семпла менше -Threshold, зробити його рівним-Threshold.
  3. Помножити значення семпла на Threshold.
Повертаємося до написаного мною синтезатору (огляд архітектури класів першої статті). Клас Distortion буде успадковувати клас SyntageAudioProcessorComponentWithparameters<AudioProcessor> і реалізовувати інтерфейс IProcessor.
Додамо параметр Power для позначення роботи ефекту. Параметр Treshold не може бути дорівнює 0, інакше нам доведеться ділити на 0. Для обмеження сигналу візьмемо максимум від значення семпла і Treshold, якщо значення семпла більше нуля; візьмемо максимум від значення семпла і -Treshold, якщо значення семпла менше нуля.
public enum EPowerStatus
{
Off,
On
}

public class Distortion : SyntageAudioProcessorComponentWithparameters<AudioProcessor>, IProcessor
{
public EnumParameter<EPowerStatus> Power { get; private set; }

public RealParameter Treshold { get; private set; }

public Distortion(AudioProcessor audioProcessor) :
base(audioProcessor)
{
}

public override IEnumerable<Parameter> CreateParameters(string parameterPrefix)
{
Power = new EnumParameter<EPowerStatus>(parameterPrefix + "Pwr", "Power", "", 'false');
Treshold = new RealParameter(parameterPrefix + "Trshd", "Treshold", "Trshd", 0.1, 1, 0.01);

return new List<Parameter> {Power, Treshold};
}

public void Process(IAudioStream stream)
{
if (Power.Value == EPowerStatus.Off)
return;

var count = Processor.CurrentStreamLenght;
for (int i = 0; i < count; ++i)
{
var treshold = Treshold.Value;

stream.Channels[0].Samples[i] = DistortSample(stream.Channels[0].Samples[i], treshold);
stream.Channels[1].Samples[i] = DistortSample(stream.Channels[1].Samples[i], treshold);
}
}

private static double DistortSample(double sample, double treshold)
{
return ((sample > 0) ? Math.Min(sample, treshold) : Math.Max(sample, -treshold)) / treshold;
}
}


Ділей і реверберація
Ділей, воно ж ехо — ефект повторення сигналу з затримкою. Зазвичай під ділеем розуміють чітке повторення (багаторазові повторення) сигналу. Увійдіть в арку, перехід — ви почуєте, як короткий голосний звук буде відображено кілька разів, втрачаючи гучність. Якщо ж стояти в концертному залі, з набагато складною архітектурою і відбивають звук поверхнями, ніж арка в будинку — ви вже можете не почути чітких повторень, а плавно затухаючий звук.
Реверберація — це процес поступового зменшення інтенсивності звуку при його багаторазових відображеннях. Прийняте час реверберації — час, за який рівень звуку зменшується на 60 dB. Залежно від пристрою кімнати/зали час реверберації і звукова картина можуть дуже сильно відрізнятися.
Слухати завжди краще, ніж читати про звук. А можна і переглянути.
Слід згадати про реалізацію ефекту реверберації за допомогою згортки Convolution Reverb). Суть в тому, що маючи на руках спеціальний файл, "описує" потрібне нам приміщення (імпульсний відгук), можна абсолютно точно відтворити реверберацію від потрібного звуку в цьому приміщенні.
Для отримання імпульсних відгуків (їх називають просто імпульси/impulses, яких дуже багато в мережі) потрібно встановити мікрофон в потрібному приміщенні, включити запис і відтворення звуку — "імпульс" — вірніше, максимально наближений до нього явище: наприклад, який-небудь гранично різкий удар; записати відлуння нашого імпульсу.
Ми отримали спосіб повністю відтворити акустику приміщення — принаймні в тій мірі, в якій нам це гарантує незмінність звуку при незмінності імпульсної функції. Не всі параметри процесу визначаються імпульсної функцією, але більшість важливих для людини — все ж визначається.
Схожим чином роблять імпульсні відгуки гітарних кабінетів, для використання їх в реампинге гітар на комп'ютері.

Кодим ефект ділею
Эхо — це повторення сигналу з деякою тимчасовою затримкою. Тобто, поточне значення сигналу складається як поточне нове значення плюс значення сигналу t часу тому, t — час затримки.
Проста формула для значення семпла:

Де x — вхідна послідовність семплів, y — результуюча, T — затримка в семплах.
Потрібно зберігати останні T розрахованих семплів. Кожен раз потрібно буде отримувати значення семпла з затримкою і зберігати нове розраховане значення. Для цих цілей підійде циклічний буфер.

Гітарна педаль Ibanez AD9 Analog Delay
Щоб регулювати гучність (я б сказав "кількість") ділея, можна в формулу підставити множники. Зазвичай в плагінах використовують терміни Dry/Wet — співвідношення в міксі необробленого ("сухого") і обробленого ("мокрого") сигналів. В сумі коефіцієнти дорівнюють 1, так як позначають частки. На фотографії педалі параметр Wet називається Delay Level.
У цій формулі немає загасання луни, воно завжди буде повторюватися з таким же рівнем гучності. Такий параметр зазвичай називають Feedback (на фотографії параметр називається Repeat), він знижує гучність в залежності від часу.

Виходить, у нашому простому ділею буде 4 параметри:
  1. Power — працює ефект чи ні
  2. DryLevel
  3. Time — час ділея в секундах
  4. Feedback
Щоб знайти T (затримка в семплах, він же розмір буфера семплів) потрібно помножити частоту дискретизації на параметр Time. Щоб кожен раз не виділяти пам'ять під буфер при зміні параметра Time, відразу створимо масив максимальної довжини Time.Max * SampleRate.
Напишемо допоміжний клас для циклічного буфера:
class Buffer
{
private int _index;
private readonly double[] _data;

public Buffer(int length)
{
_data = new double[length];
_index = 0;
}

public double Current
{
get { return _data[_index]; }
set { _data[_index] = value; }
}

public void Increment(int currentLength)
{
_index = (_index + 1) % currentLength;
}

public void Clear()
{
Array.Clear(_data, 0, _data.Length);
}
}

Функція для розрахунку семпла:
private double ProcessSample(double sample, Buffer buffer)
{
var dry = DryLevel.Value;
var wet = 1 - dry;

var output = dry * sample + wet * buffer.Current;
buffer.Current = sample + Feedback.Value * buffer.Current;

int length = (int)(Time.Value * Processor.SampleRate);
buffer.Increment(length);

return output;
}

Код класу Delay
public class Delay : SyntageAudioProcessorComponentWithparameters<AudioProcessor>, IProcessor
{
private class Buffer
{
private int _index;
private readonly double[] _data;

public Buffer(int length)
{
_data = new double[length];
_index = 0;
}

public double Current
{
get { return _data[_index]; }
set { _data[_index] = value; }
}

public void Increment(int currentLength)
{
_index = (_index + 1) % currentLength;
}

public void Clear()
{
Array.Clear(_data, 0, _data.Length);
}
}

private Buffer _lbuffer;
private Buffer _rbuffer;

public EnumParameter<EPowerStatus> Power { get; private set; }
public RealParameter DryLevel { get; private set; }
public RealParameter Time { get; private set; }
public RealParameter Feedback { get; private set; }

public Delay(AudioProcessor audioProcessor) :
base(audioProcessor)
{
audioProcessor.OnSampleRateChanged += OnSampleRateChanged;
audioProcessor.PluginController.ParametersManager.OnProgramChange += ParametersManagerOnProgramChange;
}

public override IEnumerable<Parameter> CreateParameters(string parameterPrefix)
{
Power = new EnumParameter<EPowerStatus>(parameterPrefix + "Pwr", "Power", "", 'false');
DryLevel = new RealParameter(parameterPrefix + "Dry", "Dry Level", "Dry", 0, 1, 0.01);
Time = new RealParameter(parameterPrefix + "Sec", "Delay Time", "Time", 0, 5, 0.01);
Feedback = new RealParameter(parameterPrefix + "Fbck", "Feedback", "Feedback", 0, 1, 0.01);

return new List<Parameter> {Power, DryLevel, Time, Feedback};
}

public void ClearBuffer()
{
_rbuffer?.Clear();
_lbuffer?.Clear();
}

public void Process(IAudioStream stream)
{
if (Power.Value == EPowerStatus.Off)
return;

var leftChannel = stream.Channels[0];
var rightChannel = stream.Channels[1];

var count = Processor.CurrentStreamLenght;
for (int i = 0; i < count; ++i)
{
leftChannel.Samples[i] = ProcessSample(leftChannel.Samples[i], i, _lbuffer);
rightChannel.Samples[i] = ProcessSample(rightChannel.Samples[i], i, _rbuffer);
}
}

private void OnSampleRateChanged(object sender, SyntageAudioProcessor.SampleRateEventArgs e)
{
var size = (int)(e.SampleRate * Time.Max);
_lbuffer = new Buffer(size);
_rbuffer = new Buffer(size);
}

private void ParametersManagerOnProgramChange(object sender, ParametersManager.ProgramChangeEventArgs e)
{
ClearBuffer();
}

private double ProcessSample(double sample, int sampleNumber, Buffer buffer)
{
var dry = DryLevel.Value;
var wet = 1 - dry;

var output = dry * sample + wet * buffer.Current;
buffer.Current = sample + Feedback.ProcessedValue(sampleNumber) * buffer.Current;

int length = (int)(Time.ProcessedValue(sampleNumber) * Processor.SampleRate);
buffer.Increment(length);

return output;
}
}


Модуляція параметрів
На даному етапі розглянуто і закодена наступна ланцюжок генерування звуку (все це ви знайдете в попередніх статтях):
  1. Генерування простий хвилі в осцилляторе
  2. Обробка сигналу ADSR-огинаючої
  3. Обробка сигналу фільтром частот
  4. Подальша обробка ефектами: Distrotion, Delay
Після ефектів сигнал зазвичай проходить майстер-обробку (зазвичай просто регулювання результуючої гучності) і подається на вихід плагіна.
Маючи таку послідовність, вже можна отримати найрізноманітніші звучання.
Дуже багато звуків зроблені зміною якого-небудь параметра в часі. Наприклад, в звуці пострілу "лазерного пістолета" можна виразно чути, як основна частота змінюється з вищої на нижчу.
По ідеї, про всі наші параметри (клас Parameter) знає хост, вони існують не тільки всередині нашої архітектури. У хості можна зробити автоматизацію параметра, щоб міняти їх з часом.

Автоматизація параметрів в FL Studio 12. Зверху трек "Cerbera" — доріжка з нотами для синтезатора Sytrus, нижче треки з графіками зміни параметрів (Vocodex, Flangus, Delay, Reverb) доданих VST ефектів на даному треку синтезатора
Звичайно, така автоматизація дуже зручна і дуже часто використовується при створенні музики. Але така автоматизація буде працювати тільки при програванні треку, її складно налаштовувати. Якщо ми хочемо, щоб якийсь параметр змінювався при кожному натисканні ноти, або просто постійно змінювався за будь-яким законом? Логічніше зробити автоматизацію вже в самому плагіні — буде більший простір для роботи створення звуку.
Зазвичай в синтезаторах є спеціальна частина/блок/модуль, що відповідає за модуляція параметрів. Її так і називають, блок модуляції або матриця модуляції. Модуляція параметрів схожа на модуляція амплітуди ADSR-огинаючої з 2-ї статті. Тільки тепер уявіть, що замість огинаючої можна придумати взагалі будь-закон зміни параметра, і модулювати будь-який параметр у плагіні (який має на увазі, що його можна модулювати).
"закону" зазвичай беруть огинаючі і LFO (Low Frequency Oscillator — по суті, такий же осцилятор, але його семпли використовуються як множники для модуляції а не як звукова хвиля). У багатьох синтезаторах можна намалювати графік зміни параметрів, або зібрати його з заздалегідь заготовлений патернів.

Блок модуляції в синтезаторі Sylenth1. Є дві ADSR-обвідні, два LFO-генератора і модуляція на основі інших джерел (типу Velocity від натискання ноти). Для кожного джерела можна вказати два модульованих параметра і "ступінь модулювання" як проміжний множник (крутилка зліва від назви параметра).

Матриця модулювання в синтезаторі Serum. Кожна рядок описує пару "джерело — модульований параметр" з додатковими налаштуваннями (тип модуляції, множник "кількості", крива і так далі).

Огинаючі і LFO в синтезаторі Massive. Можна вручну намалювати криву зміни, з окремих патернів/шматочків.

Пишемо клас LFO

Блок модуляції в написаному мною синтезаторі
Напишемо клас LFO: його завдання буде полягати в модулировании параметрів. Осцилятори буде генерувати хвилю з амплітудою в інтервалі [-1,1], яку ми будемо використовувати як множник для параметра. LFO-осцилятор взагалі принципово нічим не відрізняється від звичайного осцилятора, який ми кодили для генерування простий хвилі. Приставка "низькочастотний" написана тому, що він може генерувати дуже низькі частоти (менше герца). Так як людина не чує ноти нижче ~20 Герц, то на нотному клавіатурі (відповідно, на основному осцилятор) немає таких низьких частот.
Осцилятор має наступні параметри: частота і тип хвилі (Sine, Triangle, Square, Noise).
Для зручного генерування таких сигналів раніше була написана функція WaveGenerator.GenerateNextSample.
Розглянемо, яким чином будемо модифікувати значення семпла. Всі параметри (клас Parameter) мають властивість RealValue, яке відображає значення параметра в інтервал [0, 1]. Це нам і потрібно. Осцилятори генерує значення в інтервалі [-1,1]. По суті, ми крутимо ручку параметра до максимуму вправо, то до максимуму вліво.
Є проблема — припустимо, значення параметра дорівнює 0.25. Щоб однаково змінювати параметр в меншу і більшу сторону, можна змінювати його лише від 0 до 0.5 (-1 відповідає 0, 1 відповідає 0.5, при 0 — параметр не змінюється і дорівнює 0.25). Таким чином, візьмемо найменший відрізок, який ділить значення параметра r: f=min(r, 1 — r).
Тепер параметр буде змінюватися в діапазоні [r — f, r + f].
Додамо ще параметр, щоб контролювати "ширину" змінюваного діапазону значень — Gain, зі значеннями в інтервалі [0, 1].
Отримуємо наступну формулу для модифікованого значення семпла:

Тепер треба вирішити, яким чином буде працювати осцилятор. Клас LFO не генерує і не модифікує масив семплів. Так само, щоб працював осцилятор, потрібно запам'ятовувати минулий час. Тому отнаследуемся від інтерфейсу IProcessor, функції Process(IAudioStream stream) будемо вважати число пройдених семплів. Якщо поділити його на SampleRate, то отримаємо пройдене час.
У синтезаторах є опція, щоб LFO синхронізувався з натисканням клавіші. Для нас це означає, що при натисканні (обробник MidiListenerOnNoteOn) потрібно скидати фазу осцилятора (скидати час на 0). За це буде відповідати параметр-перемикач MatchKey.
Функція, що розраховує значення семпла ModifyRealValue буде приймати на вхід поточне значення параметра currentValue і поточний номер семпла sampleNumber. Яким чином коректно використовувати модифіковане значення буде написано далі. Зараз потрібно зрозуміти, що функція ModifyRealValue буде викликана для кожного семпла у вхідному масиві сеплов (який у функції Process).
Одержуємо наступні методи:
public void Process(IAudioStream stream)
{
_time += Processor.CurrentStreamLenght / Processor.SampleRate;
}

public double ModifyRealValue(double currentValue, int sampleNumber)
{
var gain = Gain.Value;
if (DSPFunctions.IsZero(gain))
return currentValue;

var amplitude = GetCurrentAmplitude(sampleNumber);
gain *= amplitude * Math.Min(currentValue, 1 - currentValue);

return DSPFunctions.Clamp01(currentValue + gain);
}

private double GetCurrentAmplitude(int sampleNumber)
{
var timePass = sampleNumber / Processor.SampleRate;
var currentTime = _time + timePass;
var sample = WaveGenerator.GenerateNextSample(OscillatorType.Value, Frequency.Value, currentTime);

sample return;
}

private void MidiListenerOnNoteOn(object sender, MidiListener.NoteEventArgs e)
{
if (MatchKey.Value)
_time = 0;
}

найголовніший параметр в класі LFO — посилання/ім'я модульованого параметра. Для цього доведеться написати клас ParameterName, який буде відображати список можливих для модулювання параметрів. Отнаследуемся від IntegerParameter, значення параметра буде означати номер у послідовності параметрів у ParametersManager. Підводний камінь — потрібно вказувати максимальне значення параметра — загальна кількість параметрів, яка в процесі розробки змінюється.
class ParameterName : IntegerParameter
{
private readonly ParametersManager _parametersManager;

public ParameterName(string parameterPrefix, ParametersManager parametersManager) :
base(parameterPrefix + "Num", "LFO Parameter Number", "Num", -1, 34, 1, false)
{
_parametersManager = parametersManager;
}

public override int FromStringToValue(string s)
{
var parameter = _parametersManager.FindParameter(s);
return (parameter == null) ? -1 : _parametersManager.GetParameterIndex(parameter);
}

public override string FromValueToString(int value)
{
return (value >= 0) ? _parametersManager.GetParameter(value).Name : "--";
}
}


Інтерфейс IParameterModifier і використання актуального значення параметра
Тепер клас параметра повинен визначати, чи можлива його модуляція. У закоденном мною синтезаторі розглянуто простий випадок — є один об'єкт класу LFO, і модулювати можна не більше одного параметра.
public interface IParameterModifier
{
double ModifyRealValue(double currentValue, int sampleNumber);
}

Оскільки параметр може бути пов'язаний з одним IParameterModifier, зробимо посилання і властивість ParameterModifier. Для отримання актуального значення потрібно замість властивості Value використовувати метод ProcessedValue, для цього передати поточний номер семпла.
public abstract class Parameter
{
...

private IParameterModifier _parameterModifier;

public bool CanBeAutomated { get; }

public IParameterModifier ParameterModifier
{
get { return _parameterModifier; }
set
{
if (_parameterModifier == value)
return;

if (_parameterModifier != null
&& !CanBeAutomated)
throw new ArgumentException("Parameter cannot be automated.");

_parameterModifier = value;
}
}

public double ProcessedRealValue(int sampleNumber)
{
if (_parameterModifier == null)
return RealValue;

var modifiedRealValue = _parameterModifier.ModifyRealValue(RealValue, sampleNumber);
return modifiedRealValue;
}

...
}

public abstract class Parameter<T> : Параметр where T : struct
{
...

public T ProcessedValue(int sampleNumber)
{
return FromReal(ProcessedRealValue(sampleNumber));
}

...
}

Використання методу ProcessedValue замість Value трохи ускладнює програмування через параметр sampleNumber, який потрібно передати. Коли я написав клас LFO, довелося у всіх класах міняти Value у параметрів на ProcessedValue. В основному, семпли оброблялися у циклі, і передати sampleNumber не склало великих проблем.
В класі LFO робимо обробник зміни параметра ParameterName, і в ньому потрібно поміняти в параметра ParameterModifier на this.
Код класу LFO
public class LFO : SyntageAudioProcessorComponentWithparameters<AudioProcessor>, IProcessor, IParameterModifier
{
private double _time;
private Parameter _target;

private class ParameterName : IntegerParameter
{
private readonly ParametersManager _parametersManager;

public ParameterName(string parameterPrefix, ParametersManager parametersManager) :
base(parameterPrefix + "Num", "LFO Parameter Number", "Num", -1, 34, 1, false)
{
_parametersManager = parametersManager;
}

public override int FromStringToValue(string s)
{
var parameter = _parametersManager.FindParameter(s);
return (parameter == null) ? -1 : _parametersManager.GetParameterIndex(parameter);
}

public override string FromValueToString(int value)
{
return (value >= 0) ? _parametersManager.GetParameter(value).Name : "--";
}
}

public EnumParameter<WaveGenerator.EOscillatorType> OscillatorType { get; private set; }
public FrequencyParameter Frequency { get; private set; }
public BooleanParameter MatchKey { get; private set; }
public RealParameter Gain { get; private set; }
public IntegerParameter TargetParameter { get; private set; }

public LFO(AudioProcessor audioProcessor) :
base(audioProcessor)
{
audioProcessor.PluginController.MidiListener.OnNoteOn += MidiListenerOnNoteOn;
}

public override IEnumerable<Parameter> CreateParameters(string parameterPrefix)
{
OscillatorType = new EnumParameter<WaveGenerator.EOscillatorType>(parameterPrefix + "Osc", "LFO Type", "Osc", 'false');
Frequency = new FrequencyParameter(parameterPrefix + "Frq", "LFO Frequency", "Frq", 0.01, 1000, false);
MatchKey = new BooleanParameter(parameterPrefix + "Mtch", "LFO Phase Key Link", "Match", 'false');
Gain = new RealParameter(parameterPrefix + "Gain", "LFO Gain", "Gain", 0, 1, 0.01, false);
TargetParameter = new ParameterName(parameterPrefix, Processor.PluginController.ParametersManager);

TargetParameter.OnValueChange += TargetParameterNumberOnValueChange;

return new List<Parameter> {OscillatorType, Frequency, MatchKey, Gain, TargetParameter};
}

public void Process(IAudioStream stream)
{
_time += Processor.CurrentStreamLenght / Processor.SampleRate;
}

public double ModifyRealValue(double currentValue, int sampleNumber)
{
var gain = Gain.Value;
if (DSPFunctions.IsZero(gain))
return currentValue;

var amplitude = GetCurrentAmplitude(sampleNumber);
gain *= amplitude * Math.Min(currentValue, 1 - currentValue);

return DSPFunctions.Clamp01(currentValue + gain);
}

private double GetCurrentAmplitude(int sampleNumber)
{
var timePass = sampleNumber / Processor.SampleRate;
var currentTime = _time + timePass;
var sample = WaveGenerator.GenerateNextSample(OscillatorType.Value, Frequency.Value, currentTime);

sample return;
}

private void MidiListenerOnNoteOn(object sender, MidiListener.NoteEventArgs e)
{
if (MatchKey.Value)
_time = 0;
}

private void TargetParameterNumberOnValueChange(Parameter.EChangeType obj)
{
var number = TargetParameter.Value;
var parameter = (number >= 0) ? Processor.PluginController.ParametersManager.GetParameter(number) : null;

if (_target != null)
_target.ParameterModifier = null;

_target = parameter;

if (_target != null)
_target.ParameterModifier = this;
}
}


Висновок
На цьому цикл статей вважаю закінченим: я розповів про найбільш важливі (на мій погляд, звичайно) складові синтезатора: генерування хвилі, обробка огинаючої, фільтрація, ефекти і модуляція параметрів. Програмування місцями було далеко від ідеалу, без оптимізацій — я хотів як можна зрозуміліше написати код. Допитливий дослідник може взяти мій код і експериментувати з ним скільки завгодно — я буду цьому тільки радий! Є хороший сайт musicdsp.org з великим архівом исходников різних штук синтезу та обробки звуку, переважно на С++, дерзайте!
Дякую всім зацікавився! Впевнений, що мої статті будуть видні з гугла і допоможуть початківцям увійти в світ програмування музики і обробки сигналів. Спасибі за ваші коментарі, особливості спасибі Refridgerator.
Всім добра!
Удачі в програмуванні!


Список літератури
Не забувайте дивитися списки статей і книг в попередніх статтях.
  1. Аудиопримеры знаменитих ефектів
  2. wikisound.org/Дисторшн
  3. A Bit About Reverb
  4. Исходники різних алгоритмів DSP
  5. Поради з програмування синтезаторів
Джерело: Хабрахабр

0 коментарів

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