- PVSM.RU - https://www.pvsm.ru -

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1

Занимаясь музыкальным творчеством, я часто делаю аранжировки и записи на компьютере — используя кучу всяких VST плагинов и инструментов [1]. Стыдно признаться — я никогда не понимал, как "накручивают" звуки в синтезаторах. Программирование позволило мне написать свой синтезатор, "пропустить через себя" процесс создания звука.

Я планирую несколько статей, в которых будет пошагово рассказано, как написать свой VST плагин/инструмент: программирование осциллятора, частотного фильтра, различных эффектов и модуляции параметров. Упор будет сделан на практику, объяснение программисту простым языком, как же все это работает. Теорию (суровые выводы и доказательства) обойдем стороной (естественно, будут ссылки на статьи и книги).

Обычно плагины пишутся на C++ (кроссплатформенность, возможность эффективно реализовать алгоритмы), но я решил выбрать более подходящий для меня язык — C#; сфокусироваться на изучении самого синтезатора, алгоритмов, а не технических деталей программирования. Для создания красивого интерфейса я использовал WPF. Возможность использования архитектуры .NET дала возможность библиотека-обертка VST. NET [2].

Ниже представлен обзорный ролик моего простого синтезатора [3], полученных интересных звучаний.

Предстоит нелегкий путь, если вы готовы — добро пожаловать под кат.

Оглавление

Загадочный мир синтеза звука [4]
Звук в цифровом виде [5]
VST SDK [6]
WDL-OL и JUCE [7]
VST .NET [8]
Моя надстройка над VST .NET [9]
WPF UI [10]
UI-поток [11]
Обзор архитектуры синтезатора Syntage [12]
Настраиваем проект для создания плагина/инструмента [13]
Отладка кода [14]
Пишем простой осциллятор [15]
Список литературы [16]

Загадочный мир синтеза звука

Я очень люблю музыку, слушаю разные стили, играю на различных инструментах, и, конечно, сочиняю и записываю аранжировки. Когда я начинал использовать эмуляторы синтезаторов в звукозаписывающих программах (да и сейчас) я всегда перебирал кучу пресетов, искал подходящее звучание.

Перебирая пресеты одного синтезатора можно встретить как "ожидаемый" звук электронного синтезатора из детства (музыка из мультика Летучий Корабль [17]) так и имитацию ударных, звуков, шума, даже голоса! И все это делает один синтезатор, с одними и теми же ручками параметров. Это меня всегда удивляло, хотя я понимал: каждый звук — суть конкретная настройка всех ручек.

Недавно я решил наконец-таки разобраться, каким же образом создаётся (или, правильнее сказать, синтезируется) звук, как и почему нужно крутить ручки, как видоизменяется от эффектов сигнал (визуально и на слух). И конечно же, научиться (хотя бы понять основы) самому "накручивать" звук, копировать понравившиеся мне стили. Я решил последовать одной цитате:

"Скажи мне — и я забуду, покажи мне — и я запомню, дай мне сделать — и я пойму."
Конфуций

Конечно, все подряд делать не надо (куда столько велосипедов?), но сейчас я хочу получить знания и самое главное — поделиться ими с вами.

Цель: не углубляясь в теорию, создать простой синтезатор, сделав упор на объяснение процессов с точки зрения программирования, на практике.

В синтезаторе будут:

Все составляющие я планирую рассмотреть в нескольких статьях. В данной будет рассмотрено программирование осциллятора.

Программировать будем на C#; UI можно писать либо на WPF, либо на Windows Forms, либо вообще обойтись без графической оболочки. Плюс выбора WPF — красивая графика, которую достаточно быстро кодить, минус — только на Windows. Владельцы других ОС — не расстраивайтесь, всё-таки цель — понять работу синтезатора (а не запилить красивый UI), тем более, код, который я буду демонстрировать, можно быстро перенести, скажем, на С++.

В главах VST SDK [6] и WDL-OL и JUCE [7] я расскажу про концепцию VST, ее внутреннюю реализацию; про библиотеки-надстройки, которые хорошо подойдут для разработки серьезных плагинов. В главе VST .NET [8] я расскажу про данную библиотеку, ее минусы, мою надстройку, программирование UI.

Программирование логики синтезатора начнется с главы Пишем простой осциллятор [15]. Если вам не интересны технические стороны написания VST плагинов, вы просто хотите прочитать про, собственно, синтез (и ничего не кодить) — милости прошу сразу к этой главе.

Исходный код написанного мной синтезатора доступен на GitHub'е [3].

Звук в цифровом виде

По-сути, конечная наша цель — создание звука на компьютере. Обязательно прочитайте (хотя бы, бегло) статью на хабре "Теория звука" [22] — в ней изложены базовые знания о представлении звука на компьютере, понятия и термины.

Любой звуковой файл в компьютере в несжатом формате представляет собой массив семплов. Любой плагин, в конечном счете, принимает и обрабатывает на входе массив семлов (в зависимости от точности это будут float или double числа, либо можно работать с целыми числами). Почему я сказал массив, а не одиночный семпл? Этим я хотел подчеркнуть что обрабатывается звук в целом: если вам нужно сделать эквализацию [23], вы не сможете оперировать одним лишь семплом без информации о других.

Хотя, конечно, есть задачи, которым не важно знать, что вы обрабатываете — они рассматривают конкретный семпл. Например, задача — поднять уровень громкости в 2 раза. Мы можем работать с каждым семплом по-отдельности, и нам не нужно знать про остальные.

Мы будем работать с семплом как с float-числом от -1 до 1. Обычно, чтобы не говорить "значение семпла", можно сказать "амплитуда". Если амплитуда каких-то семплов будет больше 1 или меньше -1, произойдет клиппинг [24], этого нужно избегать.

VST SDK

VST (Virtual Studio Technology) [1] — это технология, позволяющая писать плагины для программ обработки звука. Сейчас существует большое множество плагинов, решающих различные задачи: синтезаторы, эффекты, анализаторы звука, виртуальные инструменты и так далее.

Чтобы создавать VST плагины, компания Steinberg (некоторые ее знают по программе Cubase) выпустила VST SDK, написанный на C++. Помимо технологии (или, как еще говорят, "формата плагинов") VST, есть и другие — RTAS, AAX, тысячи их [25]. Я выбрал VST, из-за большей известности, большого количества плагинов и инструментов (хотя, большинство известных плагинов поставляется в разных форматах).

На данный момент актуальная версия VST SDK 3.6.6, хотя многие продолжают использовать версию 2.4. Исторически складывается, что сложно найти DAW [26] без поддержки версии 2.4, и не все поддерживают версию 3.0 и выше (например Reaper вообще с версией 3.0 и выше не работает).

VST SDK можно скачать с официального сайта [27].
В дальнейшем мы будем работать с библиотекой VST.NET, которая является оберткой для VST 2.4.

Если вы намерены серьезно разрабатывать плагины, и хотите использовать последнюю версию SDK, то вы можете самостоятельно изучить документацию и примеры (все можно скачать с официального сайта [28]).

Сейчас я кратко изложу принципы VST SDK 2.4, для общего понимания работы плагина и его взаимодействия с DAW [26].

В Windows VST плагин версии 2.4 представляется как динамическая DLL библиотека.
Хостом мы будем называть программу, которая загружает нашу DLL. Обычно это либо программа редактирования музыки (DAW [26]), либо простая оболочка, чтобы запускать плагин независимо от других программ (например, очень часто в виртуальных инструментах с .dll плагином поставляется .exe файл, чтобы загружать плагин как отдельную программу — пианино, синтезатор).

Дальнейшие функции, перечисления и структуры вы можете найти в скачанном VST SDK [28] в исходниках из папки "VST3 SDKpluginterfacesvst2.x".

Библиотека должна экспортировать функцию со следующей сигнатурой:

EXPORT void* VSTPluginMain(audioMasterCallback hostCallback)

Функция принимает указатель на коллбэк, чтобы плагин мог получать необходимую ему информацию от хоста.

VstIntPtr (VSTCALLBACK *audioMasterCallback) (AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt)

Все делается на достаточно "низком" уровне — чтобы хост понял, что от него хотят, нужно передавать номер команды через параметр opcode. Перечисление всех опкодов хардкорные C-кодеры могут найти в перечислении AudioMasterOpcodesX. Остальные параметры используются аналогичным образом.

VSTPluginMain должна вернуть указатель на структуру AEffect, которая, по-сути, и является нашим плагином: она содержит информацию о плагине и указатели на функции, которые будет вызывать хост.

Основные поля структуры AEffect:

  • Информация о плагине. Название, версия, число параметров, число программ и пресетов (читай далее), тип плагина и прочее.
  • Фунции для запроса и установки значений параметров.
  • Функции смены пресетов/программ.
  • Фунция обработки массива семплов

    void (VSTCALLBACK *AEffectProcessProc) (AEffect* effect, float** inputs, float** outputs, VstInt32 sampleFrames)

    float** — это массив каналов, каждый канал содержит одинаковое количество семплов (количество семплов в массиве зависит от звукового драйвера и его настроек). В основном встречаются плагины, обрабатывающие моно и стерео.

  • Супер-функция, подобна audioMasterCallback.

    VstIntPtr (VSTCALLBACK *AEffectDispatcherProc) (AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt)

    Вызывается хостом, по параметру opcode определяется необходимое действие (список AEffectOpcodes). Используется, чтобы узнать дополнительную информацию о параметрах, сообщать плагину об изменениях в хосте (изменение частоты дискредитации), для взаимодействия с UI плагина.

При работе с плагином было бы очень удобно, чтобы юзер мог сохранить все настроенные ручки и переключатели. А еще круче, чтобы была их автоматизация! Например, вы можете захотеть сделать знаменитый эффект rise up [29] — тогда вам нужно менять параметр cutoff (частота среза) [30] эквалайзера во времени.

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

Хосту все равно, какая логика у параметров в плагине. Задача хоста — сохранять, загружать, автоматизировать параметры. Хосту очень удобно воспринимать параметр, как float-число от 0 до 1 — а уж плагин пусть как хочет, так его и толкует (так и сделали большинство DAW, неофициально).

Пресеты (в терминах VST SDK — programs/программы) это коллекция конкретных значений всех параметров плагина. Хост может менять/переключать/выбирать номера пресетов, узнавать их названия, аналогично с параметрами. Банки — коллекция пресетов. Банки логически существуют только в DAW, в VST SDK есть только пресеты и программы.

Поняв идею структуры AEffect можно набросать и скомпилировать простой DLL-плагинчик.
А мы пойдем дальше, на уровень выше.

WDL-OL и JUCE

Чем плоха разработка на голом VST SDK?

  • Писать всю рутину с нуля самому?.. По-любому, кто-то уже это сделал!
  • Структуры, коллбэки… а хочется чего-то более высокоуровневого
  • Хочется кроссплатформенность, чтобы код был один
  • А что насчет UI, которое легко разрабатывать!?

На сцену выходит WDL-OL [31]. Это C++ библиотека для создания кроссплатформенных плагинов. Поддерживаются форматы VST, VST3, Audiounit, RTAS, AAX. Удобство библиотеки состоит в том, что (при правильной настройке проекта) вы пишете один код, а при компилировании получаете свой плагин в разных форматах.

Как работать с WDL-OL хорошо описано в Martin Finke's Blog "Music & Programming" [32], даже есть хабр статьи-переводы на русский [33].

WDL-OL решает, по крайней мере, первые три пункта минусов разработки на VST SDK. Все, что вам нужно — корректно настроить проект (первая статья из блога), и отнаследоваться от класса IPlug.

class MySuperPuperPlugin : public IPlug
{
public:

    explicit MyFirstPlugin(IPlugItanceInfo instanceInfo);
    virtual ~MyFirstPlugin() override;

    void ProcessDoubleReplacing(double** inputs, double** outputs, int nFrames) override;
};

Теперь с чистой совестью можно реализовать функцию ProcessDoubleReplacing, которая, по сути и является "ядром" плагина. Все заботы взял на себя класс IPlug. Если его изучать, можно быстро понять, что (в формате VST) он является оберткой структуры AEffect. Коллбэки от хоста и функции для хоста превратились в удобные виртуальные функции, с понятными названиями и адекватными списками параметров.

В WDL-OL уже есть средства для создания UI. Но как по мне, все это делается с большой болью: UI собирается в коде, все ресурсы нужно описывать в .rc файле и так далее.

Помимо WDL-OL я так же узнал про библиотеку JUCE [34]. JUCE похожа на WDL-OL, решает все заявленные минусы разработки на VST SDK. Помимо всего прочего, она уже имеет в своем составе и UI-редактор, и кучу классов для работы с аудио данными. Я лично ее не использовал, поэтому советую прочитать о ней, хотя бы, на вики [35].

Если вы хотите писать серьезный плагин, тут я бы уже всерьез задумался над использованием библиотек WDL-OL или JUCE. Всю рутину они сделают за вас, а у вас же остается вся мощь языка C++ для реализации эффективных алгоритмов и кроссплатформенность — что не маловажно в мире большого количества DAW.

VST .NET

Чем же мне не угодили WDL-OL и JUCE?

  1. Моя задача — понять как программируется синтезатор, обработка аудио, эффекты, а не как собрать плагин под максимальное количество форматов и платформ. "Техническое программирование" здесь отходит на второй план (конечно, это не повод писать плохой код и не использовать ООП).
  2. Я разбалован языком C#. Опять же, этот язык, в отличие от того же C++, позволяет не думать о некоторых технических моментах.
  3. Мне нравится технология WPF в плане ее визуальных возможностей.

Страничка библиотеки — vstnet.codeplex.com [36], там есть исходники, бинарники, документация. Как я понял, библиотека находится в стадии почти доделал и забил заморозки (не реализованы некоторые редко используемые функции, пару лет нет изменений репозитория).

Библиотека состоит из трех ключевых сборок:

  1. Jacobi.Vst.Core.dll — содержит интерфейсы, определяющие поведения хоста и плагина, вспомогательные классы аудио, событий, MIDI. Большая часть является оберткой нативных структур, дефайнов и перечислений из VST SDK.
  2. Jacobi.Vst.Framework.dll — содержит базовые классы плагинов, реализующие интерфейсы из Jacobi.Vst.Core, позволяющие ускорить разработку плагинов и не писать все с нуля; классы для более высокоуровневого взаимодействия "хост-плагин", различные менеджеры параметров и программ, MIDI-сообщений, работы с UI.
  3. Jacobi.Vst.Interop.dll — Managed C++ обертка над VST SDK, которая позволяет соединить хост с загруженной .NET сборкой (вашим плагином).

Как можно делать .NET сборки, если хост ожидает простую динамическую DLL? А вот как: на самом деле хост грузит не вашу сборку, а скомпилированную DLL Jacobi.Vst.Interop, которая уже в свою очередь грузит ваш плагин в рамках .NET.

Используется следующая хитрость: допустим, вы разрабатываете свой плагин, и на выходе получаете .NET-сборку MyPlugin.dll. Нужно сделать так, чтобы хост вместо вашей MyPlugin.dll загрузил Jacobi.Vst.Interop.dll, а она загрузила ваш плагин. Вопрос, а как Jacobi.Vst.Interop.dll узнает откуда грузить вашу либу? Вариантов решения много. Разработчик выбрал вариант называть либу-обертку одинаковым именем с вашей либой, а затем искать .NET-сборку как "мое_имя.vstdll".

Работает все это следующим образом

  1. Вы скомпилировали и получили MyPlugin.dll
  2. Переименовываем MyPlugin.dll в MyPlugin.vstdll
  3. Копируем рядом Jacobi.Vst.Interop.dll
  4. Переименовываем Jacobi.Vst.Interop.dll на MyPlugin.dll
  5. Теперь хост будет грузить MyPlugin.dll (т.е. Jacobi.Vst.Interop обертку) а она, зная что ее имя "MyPlugin", загрузит вашу сборку MyPlugin.vstdll.

При загрузке вашей либы необходимо, чтобы в ней был класс, реализующий интерфейс IVstPluginCommandStub:

public interface IVstPluginCommandStub : IVstPluginCommands24
{
    VstPluginInfo GetPluginInfo(IVstHostCommandStub hostCmdStub);
    Configuration PluginConfiguration { get; set; }
}

VstPluginInfo содержит базовую о плагине — версия, уникальный ID плагина, число параметров и программ, число обрабатываемых каналов. PluginConfiguration нужна для вызывающей либы-обертки Jacobi.Vst.Interop.

В свою очередь, IVstPluginCommandStub реализует интерфейс IVstPluginCommands24, который содержит методы, вызываемые хостом: обработка массива (буфера) семплов, работа с параметрами, программами (пресетами), MIDI-сообщениями и так далее.

Jacobi.Vst.Framework содержит готовый удобный класс StdPluginCommandStub, реализующий IVstPluginCommandStub. Все что нужно сделать — отнаследоваться от StdPluginCommandStub и реализовать метод CreatePluginInstance(), который будет возвращать объект (instance) вашего класса-плагина, реализующего IVstPlugin.

public class PluginCommandStub : StdPluginCommandStub
{
    protected override IVstPlugin CreatePluginInstance()
    {
        return new MyPluginController();
    }
}

Опять же, есть готовый удобный класс VstPluginWithInterfaceManagerBase:

public abstract class VstPluginWithInterfaceManagerBase : PluginInterfaceManagerBase, IVstPlugin, IExtensible, IDisposable
{
    protected VstPluginWithInterfaceManagerBase(string name, VstProductInfo productInfo, VstPluginCategory category,
        VstPluginCapabilities capabilities, int initialDelay, int pluginID);

    public VstPluginCapabilities Capabilities { get; }
    public VstPluginCategory Category { get; }
    public IVstHost Host { get; }
    public int InitialDelay { get; }
    public string Name { get; }
    public int PluginID { get; }
    public VstProductInfo ProductInfo { get; }

    public event EventHandler Opened;

    public virtual void Open(IVstHost host);
    public virtual void Resume();
    public virtual void Suspend();
    protected override void Dispose(bool disposing);
    protected virtual void OnOpened();
}

Если смотреть исходный код библиотеки, можно увидеть интерфейсы, описывающие компоненты плагина, для работы с аудио, параметрами, MIDI и т.д. :

IVstPluginAudioProcessor
IVstPluginParameters
IVstPluginPrograms
IVstHostAutomation
IVstMidiProcessor

Класс VstPluginWithInterfaceManagerBase содержит виртуальные методы, возвращающие эти интерфейсы:

protected virtual IVstPluginAudioPrecisionProcessor CreateAudioPrecisionProcessor(IVstPluginAudioPrecisionProcessor instance);
protected virtual IVstPluginAudioProcessor CreateAudioProcessor(IVstPluginAudioProcessor instance);
protected virtual IVstPluginBypass CreateBypass(IVstPluginBypass instance);
protected virtual IVstPluginConnections CreateConnections(IVstPluginConnections instance);
protected virtual IVstPluginEditor CreateEditor(IVstPluginEditor instance);
protected virtual IVstMidiProcessor CreateMidiProcessor(IVstMidiProcessor instance);
protected virtual IVstPluginMidiPrograms CreateMidiPrograms(IVstPluginMidiPrograms instance);
protected virtual IVstPluginMidiSource CreateMidiSource(IVstPluginMidiSource instance);
protected virtual IVstPluginParameters CreateParameters(IVstPluginParameters instance);
protected virtual IVstPluginPersistence CreatePersistence(IVstPluginPersistence instance);
protected virtual IVstPluginProcess CreateProcess(IVstPluginProcess instance);
protected virtual IVstPluginPrograms CreatePrograms(IVstPluginPrograms instance);

Эти методы и нужно перегружать, чтобы реализовывать свою логику в кастомных классах-компонентах. Например, вы хотите обрабатывать семплы, тогда вам нужно написать класс, реализующий IVstPluginAudioProcessor, и вернуть его в методе CreateAudioProcessor.

public class MyPlugin : VstPluginWithInterfaceManagerBase
{
    ...
    protected override IVstPluginAudioProcessor CreateAudioProcessor(IVstPluginAudioProcessor instance)
    {
        return new MyAudioProcessor();
    }
    ...
}
...
public class MyAudioProcessor : VstPluginAudioProcessorBase // используем готовый класс из либы
{
    public override void Process(VstAudioBuffer[] inChannels, VstAudioBuffer[] outChannels)
    {
        // обработка семплов
    }
}

Используя различные готовые классы-компоненты можно сосредоточиться на программировании логики плагина. Хотя, вам никто не мешает реализовывать все самому, как хочется, основываясь только на интерфейсах из Jacobi.Vst.Core.

Для тех, кто уже кодит — предлагаю вам пример просто плагина, который понижает громкость на 6 дБ (для этого нужно умножить семпл на 0.5, почему — читай в статье про звук [22]).

Пример просто плагина

using Jacobi.Vst.Core;
using Jacobi.Vst.Framework;
using Jacobi.Vst.Framework.Plugin;

namespace Plugin
{
    public class PluginCommandStub : StdPluginCommandStub
    {
        protected override IVstPlugin CreatePluginInstance()
        {
            return new MyPlugin();
        }
    }

    public class MyPlugin : VstPluginWithInterfaceManagerBase
    {
        public MyPlugin() : base(
            "MyPlugin",
            new VstProductInfo("MyPlugin", "My Company", 1000),
            VstPluginCategory.Effect,
            VstPluginCapabilities.None,
            0,
            new FourCharacterCode("TEST").ToInt32())
        {
        }

        protected override IVstPluginAudioProcessor CreateAudioProcessor(IVstPluginAudioProcessor instance)
        {
            return new AudioProcessor();
        }
    }

    public class AudioProcessor : VstPluginAudioProcessorBase
    {
        public AudioProcessor() : base(2, 2, 0) // плагин будет обрабатывать стерео
        {
        }

        public override void Process(VstAudioBuffer[] inChannels, VstAudioBuffer[] outChannels)
        {
            for (int i = 0; i < inChannels.Length; ++i)
            {
                var inChannel = inChannels[i];
                var outChannel = outChannels[i];
                for (int j = 0; j < inChannel.SampleCount; ++j)
                {
                    outChannel[j] = 0.5f * inChannel[j];
                }
            }
        }
    }
}

Моя надстройка над VST .NET

При программировании синта я столкнулся с некоторыми проблемами при использовании классов из Jacobi.Vst.Framework. Основная проблема заключалась в использовании параметров и их автоматизации.

Во первых, мне не понравилась реализация событий изменения значения; во вторых, обнаружились баги при тестировании плагина в FL Studio и Cubase. FL Studio воспринимает все параметры как float-числа от 0 до 1, даже не используя специальную функцию из VST SDK с опкодом effGetParameterProperties (функция вызывается у плагина чтобы получить дополнительную информацию о параметре). В WDL-OL реализация закомментирована с пометкой:

could implement effGetParameterProperties to group parameters, but can't find a host that supports it

Хотя, конечно же, в Cubase эта функция вызывается (Cubase — продукт компании Steinberg, которая и выпустила VST SDK).

В VST .NET этот коллбэк реализован в виде функции GetParameterProperties, возвращающей объект класса VstParameterProperties. Все равно, Cubase некорректно воспринимал и автоматизировал мои параметры.

В начале я внес правки саму библиотеку, написал автору, чтобы он дал разрешение выложить исходники в репозиторий, либо сам создал репозиторий на GitHub'е. Но внятного ответа я так и не получил, поэтому решил сделать надстройку над либой — Syntage.Framework.dll.

Помимо этого, в надстройке реализованы удобные классы для работы с UI, если вы хотите использовать WPF.

Самое время скачать исходный код моего синтезатора [3] и скомпилировать его.

Инструкция по сборке

  1. Склонировать/скачать репозиторий.
  2. В папку "Libs/" скачать и скопировать следующие библиотеки:
    1.1 Jacobi.Vst.Core.dll, Jacobi.Vst.Framework.dll, Jacobi.Vst.Interop из папки "CLR4x86Release" линк [37]
    1.2 CannedBytes.Midi.Message.dll из папки "_PackagesCodeRelease" линк [38]
    1.3 NAudio.dll линк [39]
  3. Собрать солюшн в Visual Studio в Debug.
  4. Чтобы запустить синт из студии, нужно использовать проект SimplyHost.
  5. Файлы плагина и зависимые библиотеки будут в папке "outvst":

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1 - 1

Правила использования моей надстройки просты: вместо StdPluginCommandStub юзаем SyntagePluginCommandStub, а свой плагин наследуем от SyntagePlugin.

WPF UI

В VST плагине не обязательно должен быть графический интерфейс. Я видел много плагинов без UI (одни из них — mda [40]). Большинство DAW (по крайней мере, Cubase и FL Studio) предоставят вам возможность управлять параметрами из сгенерированного ими UI.

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1 - 2

Автосгенерированный UI для моего синтезатора в FL Studio

Чтобы ваш плагин был с UI, во-первых, у вас должен быть класс, реализующий IVstPluginEditor; во-вторых, нужно вернуть его инстанс в перегруженной функции CreateEditor вашего класса плагина (наследник SyntagePlugin).

Я написал класс PluginWpfUI<T>, который непосредственно владеет WPF-окном. Здесь T — это тип вашего UserControl, являющийся "главной формой" UI. PluginWpfUI<T> имеет 3 виртуальных метода, которые вы можете перегружать для реализации своей логики:

  • public virtual void Open(IntPtr hWnd) — вызывается при каждом открытии UI плагина
  • public virtual void Close() — вызывается при каждом закрытии UI плагина
  • public virtual void ProcessIdle() — вызывается несколько раз в секунду из UI-потока, для обработки кастомной логики (базовая реализация пустая)

В своем синтезаторе Syntage я написал пару контролов — слайдер, крутилка (knob), клавиатура пианино — если вы хотите, можете их скопировать и использовать.

UI-поток (thread)

Я тестировал синтезатор в FL Studio и Cubase 5 и уверен, что, в других DAW будет тоже самое: UI плагина обрабатывается отдельным потоком. А это значит, что логики аудио и UI обрабатывается в независимых потоках. Это влечет все проблемы, или, последствия такого подхода: доступ к данным из разных потоков, критические данные, доступ к UI из другого потока...

Для облегчения решения проблем я написал класс UIThread, который, по сути, является очередью команд. Если вы в какой-то момент хотите что-то сообщить/поменять/сделать в UI, а текущий код работает не в UI-потоке, то вы можете поставить на выполнение в очередь необходимую функцию:

UIThread.Instance.InvokeUIAction(() => Control.Oscilloscope.Update());

Здесь в очередь команд помещается анонимный метод, обновляющий нужные данные. При вызове ProcessIdle все накопившиеся в очереди команды будут выполнены.

UIThread не решает всех проблем. При программировании осциллографа [41] необходимо было обновлять UI по массиву семплов, который обрабатывался в другом потоке. Пришлось использовать мьютексы.

Обзор архитектуры синтезатора Syntage

При написании синтезатора активно использовалось ООП; предлагаю вам познакомиться с получившейся архитектурой и использовать мой код. Вы можете сделать все по-своему, но в этих статьях придется терпеть мое видение)

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1 - 3

Класс PluginCommandStub нужен только чтобы создать и вернуть объект класса PluginController. PluginController предоставляет информацию о плагине, так же создает и владеет следующими компонентами:

  • AudioProcessor — класс со всей логикой обработки аудио данных
  • MidiListener — класс для обработки MIDI-сообщений (из Syntage.Framework)
  • PluginUI (наследник PluginWpfUI<View>) — класс, управляющий графическим интерфейсом синтезатора, главная форма — это UserControl "View".

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1 - 4

Чтобы обрабатывать аудиоданные есть интерфейсы IAudioChannel и IAudioStream. IAudioChannel предоставляет прямой доступ к массиву/буферу семплов (double[] Samples). IAudioStream содержит массив каналов.

Представленные интерфейсы содержат удобные методы обработки всех семплов и каналов "скопом": микширование каналов и потоков, применение метода к каждому семплу в отдельности и так далее.

Для интерфейсов IAudioChannel и IAudioStream написаны реализации AudioChannel и AudioStream. Здесь важно запомнить следующую вещь: нельзя хранить ссылки на AudioStream и AudioChannel, если они являются внешними данными в функции. Суть в том, что размеры буферов могут меняться по ходу работы плагина, буферы постоянно переиспользуются — не выгодно постоянно перевыделять и копировать память. Если вам необходимо сохранить буфер для дальнейшего использования (уж не знаю, зачем) — копируйте его в свой буфер.

IAudioStreamProvider является владельцем аудиопотоков, можно попросить создать поток функцией CreateAudioStream и вернуть поток для его удаления функцией ReleaseAudioStream.

В каждый момент времени длина (длина массива семплов) всех аудиопотоков и каналов одинакова, технически она определяется хостом. В коде ее можно получить либо у самого IAudioChannel или IAudioStream (свойство Length), так же у "хозяина" IAudioStreamProvider (свойство CurrentStreamLenght).

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1 - 5

Класс AudioProcessor является "ядром" синтезатора — в нем-то и происходит синтез звука. Класс является наследником SyntageAudioProcessor, который, в свою очередь, реализует следующие интерфейсы:

  • VstPluginAudioProcessorBase — чтобы обрабатывать буфер семплов (метод Process)
  • IVstPluginBypass — чтобы отключать логику синтезатора, если плагин находится в режиме Bypass [42]
  • IAudioStreamProvider — чтобы предоставлять аудиопотоки для генераторов

Синтез звука проходит длинную цепочку обработки: создание простой волны в осцилляторах, микширование звука с разных осцилляторов, последовательная обработка в эффектах. Логика создания и обработки звука была разделена на классы-компоненты для AudioProcessor. Каждый компонент является наследником класса SyntageAudioProcessorComponentWithParameters<T> — содержит ссылку на AudioProcessor и возможность создавать параметры.

В синтезаторе представлены следующие компоненты:

  • Input — обрабатывает сообщения о нажатии нот (MIDI-сообщения и нажатия из UI)
  • Oscillator — осциллятор [18]
  • ADSR — огибающая сигнала [19]
  • ButterworthFilter — фильтр частот [20]
  • Distortion — эффект дисторшн [43]
  • Delay — эффект эхо/дилей [21]
  • Clip — ограничивает значение всех семплов от -1 до 1.
  • LFO — модулирование параметров (обычно в синтезаторах модуляция осуществляется с использованием Low Frequency Oscillator — генератора низких частот)
  • Master — мастер-обработка (финальная обработка) сигнала. В данном случае содержит ручку главной громкости.
  • Oscillograph — осциллограф [41]
  • Routing — содержит в себе цепочку логики обработки звука

Все этапы создания звука вы можете найти в функции Routing.Process и на следующей схеме:

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1 - 6

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

Далее будет рассмотрено программирование логики класса Oscillator, а в следующих статьях будут рассмотрены другие классы-компоненты.

Чтобы использовать параметры, можно использовать абстрактный класс Parameter<T>, либо готовые реализации: EnumParameter, IntegerParameter, RealParameter и другие. Здесь важно понимать, что у параметра есть текущее значение Value типа T, и float-значение RealValue — отображающее обычное значение в отрезок [0,1] (нужно для работы с UI и хостом).

Настраиваем проект для создания плагина/инструмента

Наконец-то! Сейчас мы будем создавать плагин. Кодим мы на C#, и работаем в Visual Studio.
Создаем обычную .NET Class Library, и импортируем ссылки на Jacobi.Vst.Core.dll и Jacobi.Vst.Framework.dll, Syntage.Framework.dll.

Настроим копирование и переименование файлов при успешной компиляции проекта (зачем это нужно было написано в главе VST .NET [8]).

Предлагаю вам использовать следующий скрипт (его нужно прописать в Project → Properties → Build Events → Post-build event command line, выполнение скрипта поставьте на On successful build):

if not exist "$(TargetDir)vst" mkdir "$(TargetDir)vst"
copy "$(TargetDir)$(TargetFileName)" "$(TargetDir)vst$(TargetName).net.vstdll"
copy "$(TargetDir)Syntage.Framework.dll" "$(TargetDir)vstSyntage.Framework.dll"
copy "$(TargetDir)Jacobi.Vst.Interop.dll" "$(TargetDir)vst$(TargetName).dll"
copy "$(TargetDir)Jacobi.Vst.Core.dll" "$(TargetDir)vstJacobi.Vst.Core.dll"
copy "$(TargetDir)Jacobi.Vst.Framework.dll" "$(TargetDir)vstJacobi.Vst.Framework.dll"

Отладка кода

В файле моего проекта Syntage вы найдете сборку SimplyHost. Это простой хост, который на старте загружает плагин с расширением ".vstdll" (файл ищется рядом с .exe или в дочерних папках). Рекомендую вам скопировать его к себе в проект — тогда вы без проблем сразу сможете отлаживать свой плагин.

Вы так же можете использовать другие хосты для отладки, но сделать это будет уже сложнее. Когда я тестировал синтезатор, я использовал две DAW: FL Studio 12 и Cubase 5. Если в FL Studio загрузить плагин, можно из Visual Studio приконнектиться к процессу FL Studio (Debug → Attach To Process). Это не всегда работает, нужно быть очень внимательным: загружаемая .dll должна соответствовать вашему коду в студии (пересоберите проект перед отладкой); коннектиться к процессу можно только после загрузки вашего плагина в DAW.

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1 - 7

Пишем простой осциллятор

Я надеюсь, что вы прочитали главу "Обзор архитектуры синтезатора Syntage" — я буду объяснять все в терминах своей архитектуры.

Самый простой звук — это чистый тон (синусоидальный сигнал, синус) определенной частоты. В природе вы вряд ли сможете услышать чистый тон. В жизни же можно услышать чистые тона в какой-нибудь электронике (и то, уверенности мало). Фурье сказал [44], что любой звук можно представить как одновременное звучание тонов разной частоты и громкости. Окраска звука характеризуется тембром [45] — грубо говоря, описанием соотношения тонов в этом звуке (спектром [46]).

Мы пойдем схожим путем — будем генерировать простой сигнал, а затем воздействовать на него и менять с помощью эффектов.

Какие выбрать "простые" сигналы? Очевидно, сигналы, спектр которых известен и хорошо изучен, которые легко обрабатывать. Возьмем четыре знаменитые типа сигналов:

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1 - 8

Периоды четырех типов сигналов: синус, треугольник, импульс/квадрат, пила.

Чтобы синтезировать звуки, вы должны четко представлять себе исходное звучание этих простых сигналов.

Синус имеет глухое и тихое звучание, остальные же — "острое" и громкое. Это связано с тем, что, в отличие от синуса, другие сигналы содержат большое количество других тонов (гармоник) в спектре.

Наш генерируемый сигнал будет характеризоваться двумя параметрами: типом волны и частотой.
На графике изображены периоды нужных нам волн. Заметьте, что все волны представлены в интервале от 0 до 1. Это очень удобно, так как позволяет одинаково запрограммировать расчет значений. Такой подход позволяет задать произвольную форму сигнала, я даже видел синтезаторы, где можно вручную его нарисовать.

По представленным картинкам напишем вспомогательный класс WaveGenerator, с методом GetTableSample, который будет возвращать значение амплитуды сигнала в зависимости от типа волны и времени (время должно быть в пределах от 0 до 1).

Добавим так же в тип волны белый шум [47] — он полезен в синтезе нестандартных звуков. Белый шум характеризуется тем, что спектральные составляющие равномерно распределены по всему диапазону частот. Функция NextDouble стандартного класса Random имеет равномерное распределение — таким образом, мы можем считать, что каждый сгенерированный семпл относится к некоторой гармонике. Соответственно, мы будем выбирать гармоники равномерно, получая белый шум. Нужно лишь сделать отображение результата функции из интервала [0,1] в интервал минимального и максимального значения амплитуды [-1,1].

public static class WaveGenerator
{
    public enum EOscillatorType
    {
        Sine,
        Triangle,
        Square,
        Saw,
        Noise
    }

    private static readonly Random _random = new Random();

    public static double GetTableSample(EOscillatorType oscillatorType, double t)
    {
        switch (oscillatorType)
        {
            case EOscillatorType.Sine:
                return Math.Sin(DSPFunctions.Pi2 * t);

            case EOscillatorType.Triangle:
                if (t < 0.25) return 4 * t;
                if (t < 0.75) return 2 - 4 * t;
                return 4 * (t - 1);

            case EOscillatorType.Square:
                return (t < 0.5f) ? 1 : -1;

            case EOscillatorType.Saw:
                return 2 * t - 1;

            case EOscillatorType.Noise:
                return _random.NextDouble() * 2 - 1;

            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}

Теперь, пишем класс Oscillator, который будет наследником SyntageAudioProcessorComponentWithParameters<AudioProcessor>. В осцилляторе рождается звук, поэтому класс будет реализовывать интерфейс IGenerator, а именно функцию

IAudioStream Generate();

Необходимо запросить у IAudioStreamProvider (для нас это будет родительский AudioProcessor) аудиопоток, и в каждом вызове функции Generate заполнять его сгенерированными семплами.

Пока что у нашего осциллятора будет два параметра:

  • Тип волны — WaveGenerator.EOscillatorType, используем класс EnumParameter из Syntage.Framework
  • Частота сигнала — слышимый диапазон от 20 до 20000 Гц, используем класс FrequencyParameter из Syntage.Framework

Оформим все вышесказанное:

public class Oscillator : SyntageAudioProcessorComponentWithParameters<AudioProcessor>, IGenerator
{
    private readonly IAudioStream _stream; // поток, куда будем генерировать семплы
    private double _time;

    public EnumParameter<WaveGenerator.EOscillatorType> OscillatorType { get; private set; }
    public RealParameter Frequency { get; private set; }

    public Oscillator(AudioProcessor audioProcessor) :
        base(audioProcessor)
    {
        _stream = Processor.CreateAudioStream(); // запрашиваем поток
    }

    public override IEnumerable<Parameter> CreateParameters(string parameterPrefix)
    {
        OscillatorType = new EnumParameter<WaveGenerator.EOscillatorType>(parameterPrefix + "Osc", "Oscillator Type", "Osc", false);
        Frequency = new FrequencyParameter(parameterPrefix + "Frq", "Oscillator Frequency", "Hz");

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

    public IAudioStream Generate()
    {
        _stream.Clear(); // очищаем все, что было раньше

        GenerateToneToStream(); // самое интересное

        return _stream;
    }
}

Осталось написать функцию GenerateToneToStream.

Каждый раз когда мы будем генерировать семплы сигнала, мы должны помнить о двух значениях:

  • длина текущего буфера
  • частота дискретизации

Оба параметра могут меняться во время работы плагина, поэтому не советую каким-либо образом их кешировать. Каждый вызов функции Generate() на вход плагину подается буфер конечной длины (длина определяется хостом, по времени она достаточно короткая) — звук генерируется "порциями". Мы должны запоминать, сколько времени прошло с момента начала генерирования волны, чтобы звук был "непрерывным". Пока что звук будем генерировать с момента старта плагина. Синхронизировать звук с нажатием клавиши будем в следующей статье.

Семплы генерируются в цикле от 0 до [длина текущего буфера].

Частота дискретизации — число семплов в секунду. Время, которое проходит от начала одного семпла до другого равно timeDelta = 1/SampleRate. При частоте дискретизации 44100 Гц это очень маленькое время — 0.00002267573 секунды.

Теперь мы можем знать, сколько времени в секундах прошло с момента старта до текущего семпла — заведем переменную _time и будем прибавлять к ней timeDelta каждую итерацию цикла.

Чтобы воспользоваться функцией WaveGenerator.GetTableSample нужно знать относительное время от 0 до 1, где 1 — период волны. Зная нужную частоты волны, мы знаем и ее период — значение, обратное частоте.

Нужное относительное время мы можем получить как дробную часть деления прошедшего времени на период волны.

Пример: мы генерируем синус со знаменитой частотой 440 Гц [48]. Из частоты находим период синуса: 1/440 = 0.00227272727 секунды.
Частота дискретизации 44100 Гц.
Рассчитаем 44150-й семпл, если на нулевом семпле время равнялось нулю.
На 44150-м семпле прошло 44150/44100 = 1.00113378685 секунд.
Смотрим, сколько это в периодах — 1.00113378685/0.00227272727 = 440.498866743.
Отбрасываем целую часть — 0.498866743. Именно это значение и нужно передать в функцию WaveGenerator.GetTableSample.

Если записать все символьно, получим:

Программирование&Музыка: понимаем и пишем VSTi синтезатор на C# WPF. Часть 1 - 9

Оформим выкладки в виде отдельной функции WaveGenerator.GenerateNextSample и запишем итоговую функцию GenerateToneToStream.

public static double GenerateNextSample(EOscillatorType oscillatorType, double frequency, double time)
{
    var ph = time * frequency;
    ph -= (int)ph; // реализация frac вычитанием целой части

    return GetTableSample(oscillatorType, ph);
}
...
private void GenerateToneToStream()
{
    var count = Processor.CurrentStreamLenght; // сколько семплов нужо сгенерировать
    double timeDelta = 1.0 / Processor.SampleRate; // столько времени разделяет два соседних семпла

    // кешируем ссылки на каналы, чтобы было меньше обращений в цикле
    var leftChannel = _stream.Channels[0];
    var rightChannel = _stream.Channels[1];

    for (int i = 0; i < count; ++i)
    {
        // Frequency и OscillatorType лучше не кешировать - это параметры плагина и
        // они могут меняться
        var frequency = DSPFunctions.GetNoteFrequency(Frequency.Value);
        var sample = WaveGenerator.GenerateNextSample(OscillatorType.Value, frequency, _time);

        leftChannel.Samples[i] = sample;
        rightChannel.Samples[i] = sample;

        _time += timeDelta;
    }
}

Обычно, в параметры осциллятора добавляют следующие:

  • Громкость
  • Подстройка (Fine) — изменение частоты генерируемой волны в большую или меньшую сторону. Можно получить эффект, похожий на wah-wah [49] если модулировать этот параметр. Если генераторов много и они смешиваются, можно делать расстройку генераторов друг относительно друга.
  • Панировка/Стерео (Pan/Panning/Stereo) отношение громкостей сигнала в левом и правом ухе.

Данные параметры есть в реализованном мною синтезаторе — вы можете самостоятельно их реализовать.

Осталось реализовать классы AudioProcessor (будет создавать осциллятор и вызывать у него метод Generate) и PluginController (создает AudioProcessor).
Посмотрите реализацию данных классов в моем коде Syntage. На текущем этапе AudioProcessor нужен, чтобы:

  • Создать осциллятор
  • Заполнить параметры (вызвать функцию CreateParameters)
  • В функции обработки буфера семплов вызывать метод Generte у осциллятора

Простая реализация перечисленных классов, для ленивых

public class PluginCommandStub : SyntagePluginCommandStub<PluginController>
{
    protected override IVstPlugin CreatePluginInstance()
    {
        return new PluginController();
    }
}
...
public class PluginController : SyntagePlugin
{
    public AudioProcessor AudioProcessor { get; }

    public PluginController() : base(
        "MyPlugin",
        new VstProductInfo("MyPlugin", "TestCompany", 1000),
        VstPluginCategory.Synth,
        VstPluginCapabilities.None,
        0,
        new FourCharacterCode("TEST").ToInt32())
    {
        AudioProcessor = new AudioProcessor(this);

        ParametersManager.SetParameters(AudioProcessor.CreateParameters());
        ParametersManager.CreateAndSetDefaultProgram();
    }

    protected override IVstPluginAudioProcessor CreateAudioProcessor(IVstPluginAudioProcessor instance)
    {
        return AudioProcessor;
    }
}
...
public class AudioProcessor : SyntageAudioProcessor
{
    private readonly AudioStream _mainStream;

    public readonly PluginController PluginController;

    public Oscillator Oscillator { get; }

    public AudioProcessor(PluginController pluginController) :
        base(0, 2, 0) // у нас синт, на вход он не принимает данные, а только генерирует стерео-сигнал
    {
        _mainStream = (AudioStream)CreateAudioStream();

        PluginController = pluginController;

        Oscillator = new Oscillator(this);
    }

    public override IEnumerable<Parameter> CreateParameters()
    {
        var parameters = new List<Parameter>();

        parameters.AddRange(Oscillator.CreateParameters("O"));

        return parameters;
    }

    public override void Process(VstAudioBuffer[] inChannels, VstAudioBuffer[] outChannels)
    {
        base.Process(inChannels, outChannels);

        // генерируем семплы
        var stream = Oscillator.Generate();

        // копируем полученный stream в _mainStream
        _mainStream.Mix(stream, 1, _mainStream, 0);

        // отправляем результат
        _mainStream.WriteToVstOut(outChannels);
    }
}

В следующей статье я расскажу как написать ADSR-огибающую [19].

Удачи в программировании!

P.S. В заголовке я писал что занимаюсь музыкой — если кому то интересно, можете послушать мою музыку [50], и в частности записанный diy-альбом [51].

Список литературы

  1. Теория звука. Что нужно знать о звуке, чтобы с ним работать. Опыт Яндекс.Музыки. [22]
  2. Марпл-мл. С. Л. Цифровой спектральный анализ и его приложения.
  3. Айфичер Э., Джервис Б. — Цифровая обработка сигналов. Практический подход.
  4. Martin Finke's Blog "Music & Programming" [32] цикл статей по созданию синта от и до на C++, используя библиотеку WDL-OL.
  5. Хабр-переводы Martin Finke's Blog [33]
  6. Модульные аналоговые синтезаторы [52] (большая хабр-статья затрагивающая вопросы синтеза звука, обзора аналоговых синтезаторов и их составляющих).

Автор: lis355

Источник [53]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/193421

Ссылки в тексте:

[1] VST плагинов и инструментов: https://ru.wikipedia.org/wiki/Virtual_Studio_Technology

[2] VST. NET: https://vstnet.codeplex.com/

[3] моего простого синтезатора: https://github.com/lis355/Syntage

[4] Загадочный мир синтеза звука: #Part1

[5] Звук в цифровом виде: #Part2

[6] VST SDK: #Part3

[7] WDL-OL и JUCE: #Part4

[8] VST .NET: #Part5

[9] Моя надстройка над VST .NET: #Part6

[10] WPF UI: #Part7

[11] UI-поток: #Part8

[12] Обзор архитектуры синтезатора Syntage: #Part9

[13] Настраиваем проект для создания плагина/инструмента: #Part10

[14] Отладка кода: #Part11

[15] Пишем простой осциллятор: #Part12

[16] Список литературы: #Part13

[17] Летучий Корабль: https://www.youtube.com/watch?v=Zv5lIQPRCPY

[18] генератор волны (осциллятор): http://ru.wikipedia.org/wiki/Генератор_сигналов

[19] ADSR огибающая сигнала: http://ru.wikipedia.org/wiki/ADSR-огибающая

[20] фильтр частот: http://ru.wikipedia.org/wiki/Цифровой_фильтр

[21] эхо/дилей: http://ru.wikipedia.org/wiki/Дилэй

[22] статью на хабре "Теория звука": https://habrahabr.ru/company/yandex/blog/270765/

[23] эквализацию: http://ru.wikipedia.org/wiki/Эквалайзер

[24] клиппинг: http://ru.wikipedia.org/wiki/Клиппинг_(аудио)

[25] тысячи их: https://en.wikipedia.org/wiki/Audio_plug-in

[26] DAW: http://en.wikipedia.org/wiki/Digital_audio_workstation

[27] официального сайта: http://www.steinberg.net/en/company/developer.html

[28] официального сайта: http://www.steinberg.net/en/company/developers.html

[29] знаменитый эффект rise up: https://www.audioblocks.com/stock-audio/sci-fi-rise-and-and-vanish-sound-101335.html

[30] cutoff (частота среза): http://fierymusic.ru/rabota-so-zvukom/obrabotka-zvuka/chto-takoe-ekvalayzer

[31] WDL-OL: https://github.com/olilarkin/wdl-ol

[32] Martin Finke's Blog "Music & Programming": http://www.martin-finke.de/blog

[33] хабр статьи-переводы на русский: https://habrahabr.ru/post/224911/

[34] JUCE: http://www.juce.com

[35] хотя бы, на вики: http://en.wikipedia.org/wiki/JUCE

[36] vstnet.codeplex.com: http://vstnet.codeplex.com

[37] линк: https://vstnet.codeplex.com/downloads/get/910521

[38] линк: https://midinet.codeplex.com/downloads/get/664848

[39] линк: https://naudio.codeplex.com/downloads/get/1436305

[40] mda: http://mda.smartelectronix.com/

[41] осциллографа: http://ru.wikipedia.org/wiki/Осциллограф

[42] Bypass: http://ru.wikipedia.org/wiki/Байпас

[43] дисторшн: http://ru.wikipedia.org/wiki/Дисторшн

[44] Фурье сказал: https://habrahabr.ru/post/196374/

[45] тембром: http://ru.wikipedia.org/wiki/Тембр

[46] спектром: https://en.wikipedia.org/wiki/Spectrum

[47] белый шум: http://ru.wikipedia.org/wiki/Белый_шум

[48] 440 Гц: http://ru.wikipedia.org/wiki/Ля_(нота)

[49] wah-wah: http://ru.wikipedia.org/wiki/Wah-wah

[50] мою музыку: https://lartsov.bandcamp.com/album/music

[51] записанный diy-альбом: https://lartsov.bandcamp.com/album/17

[52] Модульные аналоговые синтезаторы: https://habrahabr.ru/post/236703/

[53] Источник: https://habrahabr.ru/post/311220/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best