Pull to refresh

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

Reading time 16 min
Views 11K

Всем привет! Вы читаете четвертую часть статьи про создание 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);

    return sample;
}

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> : Parameter 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);

        return sample;
    }

    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. Советы по программированию синтезаторов
Tags:
Hubs:
+20
Comments 6
Comments Comments 6

Articles