Как написать свой vst плагин

В данной теме будет рассмотрен процесс создания ВСТ плагина, на примере создания эффекта кольцевой модуляции (ring modulation). Если Вам что-то непонятно — прочитайте вводную статью, или обратитесь за помощью на форум, где есть спец. раздел по программированию.

Справка: ring modulation — звуковой эффект или соответствующее устройство, реализующее перемножение двух исходных сигналов. Своё название получил из-за технической реализации — в аналоговой схеме он содержит кольцо из четырёх диодов.

Процесс разработки плагина разделён на четыре шага:
1 — Плагин, который не имеет эффекта.
2 — Модулятор с треугольной формой волны
3 — Модулятор с синусойдной формой волны
4 — Модулятор с синусойдной формой волны и контролем генератора низких частот.

За исключением стандартных библиотек SDK (необходимых в каждом ВСТ-плагине), все представленные алгоритмы разработаны специально для этого проекта.

SDK
Стеинберг, компания-разработчик программного обеспечения, ввела на рынок ВСТ технологию, благодаря которой к хост-программе (любой секвенсор, виртуальная студия) подключаются внешние модули (ВСТ плагины) и синтезаируют или обрабатывают звук (в зависимости от функциональности). Компания предоставляет SDK 3-им лицам, для собственной разработки плагинов и их последующей популяризации. Скачать SDK можно с оф. сайта (смотри вводную статью).

Основы плагинов
ВСТ плагин — это единичный *.dl файл, который размещается в дирректории, которая задаётся при установке хост-программ (возможно изменить в настройках). При запуске хост программа анализирует папку ВСТ-инструментов, для работы с плагинами их нужно отдельно импортировать в проект (в кубейсе — F10).
Самый быстрый способ начать создавать новые плагины — это открыть пример плагина в SDK «vst2examples.dsw». В данной работе также были взяты основы этих плагинов, в качестве начального материала с корректной структурой. Плагины написаны с помощью языка C++ (также возможно программирование на дельфи, йава, .net, однако большинство плагинов пишется на С++). Хорошие знания языка С++, понимание DSP технологий — это то что Вы должны иметь для написания интересного и успешного плагина.

Элементы SDK
SDK содержит много *.h, *.hpp, и *.cpp файлов, уже размещённых на своих местах. Из всех этих файлов только один *.cpp файл будет активно редактироваться. Стеинберг просит не редактировать другие файлы SDK (хост программы будут их использовать для корректной работы с плагином).

GUI
GUI — Графический интерфейс пользователя, система средств для взаимодействия пользователя с компьютером, основанная на представлении всех доступных пользователю системных объектов и функций в виде графических компонентов экрана. Вы можете сами создать дизайн своему плагину, если же Вы это не сделаете, т.е. ГУИ будет отсутствовать — хост программа автоматически сгенерирует визуализацию Вашего плагина и его контроллеров. В различных хост-программах техника визуализации плагинов отличается.

Давайте посмотрим на отображение нашего кольцевого модулятора в разных хост-программах:

Часть первая — Создание нефункционального плагина

Начинать надо с чего-то очень простого, то что интуитивно понятно. Нами был взят пример из  SDK. Самый просто плагин — это тот который ничего не делает, т.е. никак не изменяет звук. Этот пример пустого плагина поможет нам объяснить некоторые важные функции, понимание которых необходимо для дальнейшей разработки плагинов.

Функции process() и processReplacing()

Хост-программа часто воспроизводит внешние аудио-файлы. Аудио воспроизводится с жёсткого диска и микшируется в программном миксере. На этом микшере есть параметры  “send” и “insert”, как и в аналоговых микшерах. С помощью этих инструментов могут быть добавлены и использованы ВСТ плагины.  Инструмент send отправляет аудио поток к плагину, перед достижением выхода на мастере (master output). Инструмент “insert” посылает сигнал через плагин, удаляя определённые параметры (используется в сайдчейне и вокодере ). untitled-13

Выбор функции process() и processReplacing() зависит от того, хотите ли Вы использовать  “send” или “insert” оперции в качестве аудиоэффекта. Эти функции задают характер взаимодействия между хост-программой и плагином.

Для первого примера, плагин создан для приёма звука (input) и последующей передачи, без изменений, на выход ( output). Он работает как insert эффект и использует функцию processReplacing().

Просмотреть или скачать содержимое *.cpp файла в текстовом формате (пустой плагин).

Главная часть:

void AGain::processReplacing(float **inputs, float**outputs, long
sampleFrames)
{
float *in1 = inputs[0];
float *in2 = inputs[1];
float *out1 = outputs[0];
float *out2 = outputs[1];
while(—sampleFrames >= 0)
{
(*out1++) = (*in1++) ;
(*out2++) = (*in2++) ;
}
}

О коде:

Когда функция начинает действовать она имеет **inputs и **outputs, которые соответствуют входящему и выходящему буферам,  SampleFrames задаёт размер буфера. Значения входа ( input) и выхода (output) формируются из переменных  in1 и in2 (которые представляют левый и правый  входные каналы), и переменные out1 and out2 (левый и правый выходные каналы) соответственно.

До тех пор, пока  sampleFrames больше нуля — производится копирование значений со входа ( input) на выход (output) и переход к следующему значению (используется функция «++»). Как только значение sampleFrames равно нулю, буфер окончен, функция завершает своё действие. Этот пример не прдеставлен в SDK документации, и требует понимания синтаксиса языка С++.

Представление аудио в SDK

Важно знать, то что аудио информация в плагинах находится (варьирует) между +/-1.

Перед тем как переёти к следующей части, мы создали плагин, в котором на выход одного канала посылается значение +1, на выход другого канала посылается значение -1.

Код:

void AGain::processReplacing(float **inputs, float **outputs, long
sampleFrames)
{
float *in1 = inputs[0];
float *in2 = inputs[1];
float *out1 = outputs[0];
float *out2 = outputs[1];
while(—sampleFrames >= 0)
{
(*out1++) = (1) ;
(*out2++) = (-1) ;
}
}

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

untitled-14

Часть вторая — создание модулятора с треугольной формой волны

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

Во время написания кода плагина, используя в коде  DSP алгоритмы (Цифровая обработка сигналов) — очень важно понимать код, что делается со входящим потоком. Перед напиманием кода желательно вооружиться карандашом и бумагой, и нарисовать блок-схему действия будущего плагина.

Блок схема нашего модулятора:

untitled-15

Было решено то что лучший способ создания тремоло треугольной формы волны — это использовать прерывания при достижении определённого уровня звука. Изначально переменная «тремоло» имеет значение ноль. Далее она проверяется условием — «больше значения 0.1 ?» Если нет -у кровню звука добавляется опр. значение (гейн). Значение переменной «гейн» может быть ассоциировано с определённым регулятором (используется GUI — Графический интерфейс пользователя)

В нашем случае переменная «тремоло» умножается на 10 и посылается на выход.

Программа же возвращается на второй этап и продолжает действие, выясняя, стало ли значение тремоло больше значения 0.1. Процесс повторяется до достижения  значения 0.1, после чего значение «тремоло» обнуляется и  всё начинается заново.

Давайте посмотрим на форму волны входящего аудио потока:

untitled-16

Треугольную форму волны (показано для примера):

untitled-17

И на форму волны выходящего аудио-потока:

untitled-18

Просмотреть или скачать полный .cpp файл проекта

Код алгоритма:

double tremolo=0;
void AGain::processReplacing(float **inputs, float **outputs, long
sampleFrames)
{
float *in1 = inputs[0];
float *in2 = inputs[1];
float *out1 = outputs[0];
float *out2 = outputs[1];
if ( tremolo < 0.1 )
{
tremolo=(tremolo+fGain);
while(—sampleFrames >= 0)
{
(*out1++) = (*in1++) * tremolo *10;
(*out2++) = (*in2++) * tremolo *10;
}
}
if ( tremolo >= 0.1 )
{
tremolo=0;
while(—sampleFrames >= 0)
{
(*out1++) = (*in1++) * tremolo;
(*out2++) = (*in2++) * tremolo;
}
}
}

Этот код модулирует входящий аудио-поток, используя треугольную форму волны с частотой, заданной пользователем. Переменная «тремоло» описана снаружи функции  processReplacing(), — это необходимо для возможности реинициализации каждый раз, когда аудио буфер полон (завершён). Для того чтобы позволить пользователю модулировать входящий поток с определённой частотой необходимо внести некоторые изменения. Допустим, наша хост-программа работает с частотой дискретизации 44100Hz (т.е. 44100 отсчётов в секунду), эти значения могут быть использованы для задания частоты модуляции в С++ коде. Ниже показаны номера отсчётов, которые соответствуют критическим значениям:

untitled-19
untitled-20

Деля значения отсчётов на половину, четверть, три четверти и полную секунду мы получаем номера «крайних» отсчётов для модуляции.

Если переменная «тремоло» использует код моделирования треугольной формы (переименованой в counter для упрощения), добавляя опр. значения, то в «крайних отсчётах» теоретически будет преодолено значение 1, что будет происходить в нашем случае каждую  1/8 секунду.

1/11020 = 0.000090744
Если это значение (11020ая от 1-цы) будет разделено на 8, то мы получим:
0.000090744 / 8 = 0.000011343

Это значение может быть использовано пользователем для контроля частоты модуляци. Новая переменная topFreq содержит в себе максимальное значение амплитуды, после обработки модулятором. Переменная fГейн, которая содержит значения от 0 до 1, может быть умножена на переменную topFreq, для генерирования пропорции 1kHz на лету.

Просмотреть-скачать полный .cpp файл, в котором задаётся спец. частота модуляции

Код алгоритма:

float counter = 0;
double quarterSec = 0.000011343;
double topFreq = 1000;
void AGain::processReplacing(float **inputs, float **outputs, long
sampleFrames)
{
float *in1 = inputs[0];
float *in2 = inputs[1];
float *out1 = outputs[0];
float *out2 = outputs[1];
while(—sampleFrames >= 0)
{
counter = (counter + (quarterSec*(topFreq*fGain)));
if (counter > 0.5)
{
counter = 0;
}
(*out1++) = (*in1++ * counter);
(*out2++) = (*in2++ * counter);
}
}

3 — Модулятор с синусойдной формой волны

Замена треугольного модулятора синусойдной волной

Это необходимо сделать для того чтобы добиться более богатого гармониками тембра звука. Эти гармоники, после модулирования со вторым сигналом, производят вдое больше гармоник благодаря природе «сложения и разницы» кольцевой модуляции. Звук будет более музыкальным и насыщенным. Синусоидная волна может быть сгенерирована в нашем алгоритме с помощью переменной counter. Благодаря добавлению ранее использованого кода, синусойда может быть сгенерирована из серии цифр. Переменная counter должна иметь значения приблизительно от нуля до двух пи (3.14), для того чтобы создать один период синусойды.  Ниже расположены диаграммы, которые показывают: 1) Сгенерированую синусоидную волну (ампдитуду) 2) Быстрое преобразование Фурье простой синусойдной волны (спектрограмму):

untitled-21

untitled-22

Этот небольшой эксперимент был сделан для того чтобы показать то что при простом искажение волны , БПФ показывает гораздо больше гармоник, хотя до этого была только одна базовая частота. (Синусоидная форма волны имеет меньше всего гармоник, при искажении формы добавляются гармоники, квадратная форма волны имеет больше всего гармоник. Например  эффект дисторшен применяется для обогащения входящего потока гармониками путём искажения формы волны от синусойдной к более квадратной, по сути тут это и было сделано, путём завышения уровня сигнала и как следствие его искажения).

Для лучшего понимания ознакомтесь с блок схемой:

untitled-23

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

Чтобы лучше понять, ознакомтесь с диаграммами:

Входной сигусойдный сигнал:

untitled-24

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

untitled-25

Результат на выходе:

untitled-261

Алгоритм кода:

float counter = 0;
double quarterSec = (0.000570162/4);
double topFreq = 10000;
void AGain::processReplacing(float **inputs, float**outputs, long
sampleFrames)
{
float *in1 = inputs[0];
float *in2 = inputs[1];
float *out1 = outputs[0];
float *out2 = outputs[1];
while(—sampleFrames >= 0)
{
counter = (counter+(quarterSec*(topFreq*fGain)));
if (counter > (2*3.14159))
{
counter -= (2*3.14159);
}
double sinMod = sin(counter);
(*out1++) = ((*in1++)*(sinMod/2));
(*out2++) = ((*in2++)*(sinMod/2));
}
}

4 — Модулятор с синусойдной формой волны и контролем генератора низких частот.

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

Блок схема:

untitled-27

Блок схема показывает то что генерируются два независимых синусоидных сигнала. Один является модулирующим с опр. частотой, и был создан также как и в более ранних примерах. Второй является генератором низких частот ( LFO ). Пользователь сможет контролировать амплитуду и частоту сигнала, изменяя значения контролируемых через GUI переменных countertoo.

Просмотреть или скачать полный .cpp файл

Код алгоритма:

float counter = 0;
float countertoo = 0;
double quarterSec = (0.000570162/4);
double topFreq = 10000;
double LFOFreq = 10;
void ADelay::processReplacing(float **inputs, float **outputs, long
sampleFrames)
{
float *in1 = inputs[0];
float *in2 = inputs[1];
float *out1 = outputs[0];
float *out2 = outputs[1];
while(—sampleFrames >= 0)
{
countertoo = (countertoo + (quarterSec*(LFOFreq*fFeedBack)));
if (countertoo > (2*3.14159))
{
countertoo -= (2*3.14159);
}
double nuMod = sin(countertoo);
counter = (counter + (quarterSec*(topFreq*fDelay)+(nuMod*(fOut/8))));
if (counter > (2*3.14159))
{
counter -= (2*3.14159);
}
double sinMod = sin(counter);
(*out1++) = ((*in1++)*(sinMod/2));
(*out2++) = ((*in2++)*(sinMod/2));
}
}

Работа с GUI:

enum

{
variable1;
variable2;
variable3;
kNumParams
};

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

Скачать готовый, откомпилированый плагин

Автор статьи:  Toby Newman

Перевод и html вёрстка: corpuscul.net

Если Вы столкнулись с трудностями при прочтении материала — то это повод задать вопросы на форуме, в разделе «программирование».

Программирование, .NET, Алгоритмы


Рекомендация: подборка платных и бесплатных курсов Smm — https://katalog-kursov.ru/

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

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

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

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

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

Цикл статей

  1. Понимаем и пишем VSTi синтезатор на C# WPF
  2. ADSR-огибающая сигнала
  3. Продолжение следует…

Оглавление

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

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

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

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

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

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

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

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

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

  • генератор волны (осциллятор)
  • ADSR огибающая сигнала
  • фильтр частот
  • эхо/дилей
  • модуляция параметров

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

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

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

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

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

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

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

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

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

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

VST SDK

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

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

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

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

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

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

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

Дальнейшие функции, перечисления и структуры вы можете найти в скачанном VST SDK в исходниках из папки «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 — тогда вам нужно менять параметр cutoff (частота среза) эквалайзера во времени.

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

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

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

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

WDL-OL и JUCE

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

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

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

Как работать с WDL-OL хорошо описано в Martin Finke’s Blog «Music & Programming», даже есть хабр статьи-переводы на русский.

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. JUCE похожа на WDL-OL, решает все заявленные минусы разработки на VST SDK. Помимо всего прочего, она уже имеет в своем составе и UI-редактор, и кучу классов для работы с аудио данными. Я лично ее не использовал, поэтому советую прочитать о ней, хотя бы, на вики.

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

VST .NET

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

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

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

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

  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, почему — читай в статье про звук).

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

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.

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

Компилирование кода

  1. Склонировать/скачать репозиторий.
  2. Собрать солюшн в Visual Studio в Debug.
  3. Чтобы запустить синт из студии, нужно использовать проект SimplyHost.
  4. Файлы плагина и зависимые библиотеки будут в папке «outvst»:

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

WPF UI

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

Автосгенерированный 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 не решает всех проблем. При программировании осциллографа необходимо было обновлять UI по массиву семплов, который обрабатывался в другом потоке. Пришлось использовать мьютексы.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Предлагаю вам использовать следующий скрипт (его нужно прописать в 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.

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

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

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

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

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

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

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

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

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

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

Добавим так же в тип волны белый шум — он полезен в синтезе нестандартных звуков. Белый шум характеризуется тем, что спектральные составляющие равномерно распределены по всему диапазону частот. Функция 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 Гц. Из частоты находим период синуса: 1/440 = 0.00227272727 секунды.
Частота дискретизации 44100 Гц.
Рассчитаем 44150-й семпл, если на нулевом семпле время равнялось нулю.
На 44150-м семпле прошло 44150/44100 = 1.00113378685 секунд.
Смотрим, сколько это в периодах — 1.00113378685/0.00227272727 = 440.498866743.
Отбрасываем целую часть — 0.498866743. Именно это значение и нужно передать в функцию WaveGenerator.GetTableSample.

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

Оформим выкладки в виде отдельной функции 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 если модулировать этот параметр. Если генераторов много и они смешиваются, можно делать расстройку генераторов друг относительно друга.
  • Панировка/Стерео (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-огибающую.

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

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

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

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

Last updated on January 23rd, 2023

VSTs, (Virtual Studio Technologies) are audio engineering tools that process sound. 

VST’s integrate into most existing DAWs. Including Logic, Pro Tools, Ableton, F-L Studio, and many others. A music producer may need to use several VST’s in a project.

VST’s expand the sonic palette and tools available to producers. Usually, a VST produces an effect that is not easy to achieve using stock plug-ins.

The design of VST’s is a fascinating process that can be worthwhile investigating. It requires good knowledge of how digital audio works. And a basic understanding of programming. Although much of this can be learned through the process.

A VST can be created to perform any audio-based task. It provides a method for programmers to interact with audio data to create any new effect they can imagine.

Common types of VST include:

Instruments, such as synthesizers or samplers, which act as audio sources, 

Processors, or effects, which change and modify audio signals, 

Analyzers, which reads data and converts it into a graphical interface easy for humans to understand.

If you want to code vst plugins, It is recommended that you know the fundamentals of electricity, circuit design, audio circuit design, digital audio, digital signal processing, acoustics, music theory, and programming.

Understanding C++ programming would be hugely beneficial for anyone looking to make VSTs and audio software. Having an interest in graphic design and GUI programming is also a bonus.

make a VST Plugin

For audio and digital signal processing knowledge, you should at least understand and be able to explain frequency, amplitude, filters, sample rate, and bit depth. If you lack this audio DSP knowledge, it might be worth doing some background reading before learning how to make a vst.

How to make a VST Plugin:

There are several steps on the path to creating a VST plugin.

Generally, there are 4 key stages in plug-in creation:

Design > Prototype > Develop & Review > Produce.

Learn C++ programming and use a VST SDK to develop a VST plugin. Test and refine your plugin with a digital audio workstation before releasing it.

Choosing a Plugin Format:

At this stage, you should decide on the plugin format for the final product. It could be VST2, VST3, AAX, Audio Units, or all of the above. Factor the number of intended plugin formats into the development time plan.

Once the design is finalized, a prototype should be programmed. This allows the developers to test the product and gather any feedback to inspire further direction. Depending on the scale of the project, the prototyping stage of creating vst plugins could last anywhere from a few hours to weeks or months. 

To make a vst plugin, one needs to first create and compile the code, this will be done in an IDE (integrated development environment). Once the plugin has been prototyped in the IDE, it can be built into a test version for internal review and feedback.

Plugin manufacturers may turn to experts and industry professionals for private testing before releasing to the wider market.

The specifics of “how-to” depend on the style of the plugin being made. For instruments or sample-based vst plugins, additional recording and editing stages may be required.

For analog emulation plugins, the electrical flow of the hardware circuit must be measured (using SPICE or similar), to create a convolution algorithm that digitally emulates the effect of the electrical process on a signal.

Read our post on Best VST Vocoder.

Required tools to develop VST Plug-ins?

Many different methods can be used to create and develop VST plugins. Some solutions are easier and more beginner-friendly than others but may be more limited. 
For the most flexible and powerful development tools, we recommend using 

C++, Steinberg SDK, and Microsoft Visual Studio IDE.

visual studio for VST

There are other free tools like SynthEdit or  HALions Macro edit, that provide template-like vsts with visual programming tools to make vsts, however, these will be more limited in what they can produce compared to programming from scratch.

Synth Edits

MAX MSP, whilst not VST, is a streamlined and accessible way to create audio effects for Ableton Live, which some people will find much easier than the IDE method as it uses visual programming and is well documented.

Xcode 4 or later is also an option for Mac users as an IDE.

#1. JUCE Cross Platform C++ Library

C++ Library

JUCE is a great free solution for creating cross-platform (Windows/Mac/Linux/Mobile) software.

It is a popular choice in the vst and audio software developer community thanks to its extensive documentation, Graphical User Interface Tools, and Projucer – which is designed for creating audio software. 

Juce includes components that kickstart the development process. Juce also includes relevant source code for reference and modification

The Juce library is extensive and well suited for sound analysis and music production-based applications. This is a framework built with vst, au, and aax formats in mind.

Download Juce from the website and get stuck in!

#2. SynthEdit

SynthEdit is a classic “Save As” style programming tool for making vst plug ins. The environment is perfect for beginners thanks to its programming-free, drag-and-drop workflow.
Created by Jeff McClintock in 1987, he proposed it to be “Lego for audio”, encouraging users to experiment with the modules and packaged algorithms and create original effects and instruments. Many styles of GUI controls are included.

It takes away a lot of the tedious work that’s required in standard programming environments, where every low-level building block must be created from scratch. This leaves users free to experiment at an immediate level, so they can design, test, and review their concepts without having to put in a tonne of footwork.

The included modules provide a staggering amount of creative options from the get-go, so this is a great place to start for newcomers and people with no coding experience. Sorry Mac users… this one is windows only!

Download SynthEdit for free now!

#3. FL SynthMaker

FL Flower stone

Flowstone, AKA FL SynthMaker is an application based on the Ruby language that is used to create new plugins which can be used in FL Studio. It can create effects processors, synthesizers, and even experimental midi-hardware controllers that control robotics or create wacky midi controllers from WII remotes. Most GUI controls are similar to parts in FL.
Synthmaker is another visual programming application, meaning it has a toolbox full of premade components and templates that are dragged together, then further customized. This is another good place to start for beginners, but unfortunately, plugins will not work with any DAW other than FL.

 The documentation is fantastically exhaustive, so new developers will not struggle to find the information they need.

How Are VST Plugins Developed?

#1. Audio

Audio plug ins either work by acting as an audio source – via playing digital audio files or synthesis. Or they work by manipulating sound. A key stage of developing audio plug ins is understanding what process you want to happen to the sound.

If you are making a plug in involving samples, they should be stored in the vst folder.
For plug ins that work by processing a sound, for instance, EQ, Compressor, Reverb, the logic behind it will be different.

Processing plug ins need to go through a round of complex analysis to read the audio signal, then it needs to apply the process to it. Non-Linear effects may take more work.

Understand what process needs to happen to the audio signal to achieve the desired effect. For instance, with distortion, the waves are clipped at the extreme edges of their polarity. This works by amplifying the numbers until the data is too large for the AD converter, so information is lost. This creates a distorted tone.

#2. Programming

The audio plugin needs to be programmed in a way that it works with a vst host like Logic, Ableton, F-L Studio, and Pro Tools. It is wise to understand low-level code when writing digital signal processes.

A plug-in can also be programmed with a standalone application. This is a great way to make the plugin work independently of any vst hosts.

Developing cross-platform software can require more coding to integrate with each operating system. A plug in may need to be recompiled multiple times.

Programming can take a while to learn. If you are new, start by following basic examples and templates. 

Some key terms to understand: bool, integer, float, string, function, method, update.
Learn how to find and remove bugs, learn how to use the developer console.

#3. Maths

Understanding the maths needed to write plugins for audio DSP can be daunting, but do not be afraid. Many core DSP concepts revolve around the same mathematical principles and theories.

To refresh your maths chops, have a look at these practical algebra lessons: www.purplemath.com
Whether you like it or not, all music is maths. It may not seem like it, but every element of music and sound can be analyzed and interpreted into mathematical information.

Computers read and write music as maths. It measures the relative volume (db) of each individual frequency (hz) throughout a timeline (time). It becomes a grid of information.
Learning how to navigate this grid of information is important in understanding how to make vst, au, and aax plugins.

#4. Digital Signal Processing

This term refers to the use of computers to process an electronic signal. Computers are digital machines thanks to their foundations in numbers, or “digits”.

Signals refer to gestures that convey pieces of information, in this case electronic. Sounds are converted to electrical information by an analog to digital (AD) conversion process.

Computers store, read, and write sound as numerical information. These pieces of information are called Bits, which represent the energy of different frequencies at any moment in a sequence of time. In essence a form of linear algebra.
This is the basis of the sampling theorem, a concept you should understand before attempting to write audio plug ins, or any software where digital signal processes apply.

#5. Books

There are countless books on the subject of plugin development, but here are my top recommendations:

  • Designing Audio Effect Plugins in C++” — Will Pirkle
    This covers all the information needed to start making plugins in C++. It discusses the nuances of different plug in formats including vst, au, aax, and asio applications. Explains how to make all kinds of plug in from audio filters to samplers and reverb.
  • Fundamentals of Digital Audio Processing — The Scientist and Engineer’s Guide to Audio Signal Processing” Steven W Smith.
    This is a comprehensive book that covers sampling theorem, discrete-time systems, audio DSP, maths, sound analysis, sound modeling.
  • Signals, Sound, and Sensation” William M. Harmann
    The book has an introductory text on psychoacoustics and mathematics, which is enlightening for this field.

#6. Audio Plugins Frameworks

There are several audio plugin frameworks each with its own strengths and weaknesses. There isn’t a right or wrong choice, it depends on the scope and intention of the project.

  • Juce: Easy to use, well documented and supports VST, VST3, AU, RTAS, and AAX formats.
  • IPlug: A streamlined C++ framework, supports VST2, VST3, AUv2, AUv3, AAX (Native), and the Web Audio Module (WAM) plug-in APIs.
  • VST.NET: A framework used for writing .net vst host applications, some net language is needed. It’s built above the interop layer for structured architecture.
  • Delphi Library: is great for creating VST plugins, and also supports ASIO applications.

What is the Best programming language for the VST Plugins?

programming language for the VST Plugins

C++ is the most recommended language. It comes recommended by many vst plugin developers who develop professional vst plugins.
It may be harder to learn than simpler languages and have a few gripes such as having to manually program memory allocation, but C++ is an incredibly powerful and flexible code that works well for VST programming.
A further strength is that it can create plugins for a wide variety of plugin formats

Conclusion

This is an overview of how to make vst plugins. As each plugin is unique in design, the specifics will vary from build to build, but the overall process and skills are similar. 

This can become a quite complicated project, so having a clear and structured architecture for organizational or organizational elements is essential. Find your own niche and specialty within the vst scene. 

Do you want to make plugins for Ableton Live, Logic, Pro Tools, or FL Studio? 

Each DAW and operating system has its nuances.
Are you interested in creating instruments, like synths and samplers? Or are you thinking about fx processors like compressors, equalizers, reverbs, and distortion?

The process for making a virtual piano instrument will be completely different than making an analog emulating compressor plugin. 

Experiment with the variety of tools available, eventually you will find your specialization! Learning to program and develop software is a long and winding journey, but incredibly rewarding.

I studied audio engineering at university where I learned how to make vsts and music software. Feel free to contact us with any questions. Good luck!

PETELIN.RU → Статьи → SynthEdit 3: создаем VSTi своими руками


SynthEdit 3: создаем VSTi своими руками

Роман Петелин

Допустим, у вас
появилась идея создать свой
собственный синтезатор с
оригинальной архитектурой синтеза.
Конечно, задумку можно воплотить в
«железе», но для этого
требуются очень глубокие
специальные знания, богатый опыт и
очень много свободного от основной
работы времени. Более легкий путь —
реализовать синтезатор в
виртуальном виде, например, как
плагин VSTi. Сделать это можно по-разному.
В статье описан самый простой
способ.

Заранее скачайте файл с
примерами к данной статье:
http://petelin.ru/soft/se.zip.
В дальнейшем я буду ссылаться на
файлы из этого архива.

Как создать свой собственный VST-плагин?
Самый правильный путь:
воспользоваться SDK от Steinberg и
написать плагин на C++. SDK (Software Development
Kit) — набор из средств разработки,
исходных кодов программных
библиотек, утилит и документации,
который позволяет программистам
создавать приложения по
определенной технологии. SDK для
разработки VST разных версий
доступны на сайте http://ygrabit.steinberg.de.
Именно так и создается большинство
популярных коммерческих плагинов.
Да вот беда — вы не программист и нет
времени на обучение высокому
искусству программирования. Тогда
выход один — воспользоваться
специальным программным продуктом
SynthEdit (текущая версия 3), который
представляет собой визуальную
среду для проектирования VST. С его
помощью можно создавать и VST (эффекты
и обработки), и VSTi (инструменты).

В качестве примера приведу сайты,
на которых можно скачать
бесплатные VSTi, созданные с помощью
SynthEdit:

Home

http://www.spacewindow.pochta.ru

http://www.vsti.resourcez.com

http://www.e-phonic.com

http://krakli.wordpress.com

http://www.tweakbench.com

http://www.greenmachine.pwuq.net.

На сайте http://y0u-file.narod.ru вы найдете бесплатный плагин Polyvoks Station VSTi,
которому посвящена статья Ю.
Петелина «Куплю советский
синтезатор «Поливокс» и продам
«Ямаху» (http://petelin.ru/pcmagic/polyvoks/polyvoks.htm).
Он тоже создан с помощью SynthEdit.

Существуют также коммерческие
плагины, созданные с помощью SynthEdit.
Примеры:

http://www.algomusic.net

http://www.psychicmodulation.com

Softplug Products VST Plugins

Достаточно просто определить,
создан ли VST-плагин с помощью SynthEdit
или нет. Для этого нужно
просмотреть содержимое
соответствующего DLL-файла. Никаких
специальных средств просмотра не
потребуется, можно воспользоваться
обыкновенным блокнотом Windows.

  1. Найдите DLL-файл плагина,
    щелкните на нем правой кнопкой
    мыши (откроется контекстное
    меню).
  2. Выберите Открыть с помощью,
    затем нажмите кнопку Выбор
    программы
    , выберите Выбор
    программы из списка вручную
    ,
    нажмите OK, выберите Блокнот,
    нажмите OK.
  3. В программе Блокнот
    откроется содержимое DLL-файла (выглядит,
    как полная тарабарщина).
  4. Воспользуйтесь командой
    главного меню Правка >
    Найти
    для поиска
    фрагмента текста «synthedit»
    или «www.synthedit.com».

Нашли? Значит плагин сделан с
помощью SynthEdit.

Скачать SynthEdit можно на сайте http://www.synthedit.com
в разделе DOWNLOAD. SynthEdit будет работать
с некоторыми ограничениями до тех
пор, пока вы его не зарегистрируете.
Регистрация доступна в разделе
REGISTER указанного сайта и стоит 50 USD.

Незарегистрированному SynthEdit
присущи следующие ограничения:

  • ограниченное количество
    пресетов;
  • ограниченное количество
    аудиоканалов: только два
    входных и два выходных (т. е.
    стерео);
  • на панели плагина и в окне About
    (о программе) отображаются
    ссылки на то, что плагин сделан
    с помощью SynthEdit;
  • не позволяет создать
    всплывающее окно (nag screen) с
    просьбой зарегистрировать/приобрести
    плагин.

Идеология SynthEdit такова: вы словно
в детском конструкторе собираете
виртуальный инструмент из
различных модулей (осцилляторов,
генераторов, модуляторов, фильтров,
проигрывателей звуковых файлов и т.
д.). На сайте http://www.synthedit.com в разделе
MODULES можно скачать дополнительные
модули сторонних разработчиков.
Модули представляют собой файлы с
расширением SEP, которые следует
помещать в папку C:Program
FilesSynthEditmodules. Если же вы все-таки
обладаете навыками
программирования, то можете
создавать собственные модули для
SynthEdit с помощью SDK (раздел DEVELOPMENT KIT
сайта).

После запуска SynthEdit проверим
настройки аудио- и MIDI-портов: окно Preferences
открывается командой главного меню
File > Preferences. Порты
назначаются на вкладке Audio &
MIDI
. На вкладке General
можно выбрать русский язык (Language)
интерфейса. Русифицированы только
главное меню и заголовки некоторых
окон. Все модули, из которых
создаются плагины, останутся
англоязычными.

При первом запуске SynthEdit
загружается демонстрационный
проект demo.se1. Однако он слишком
сложен для начального знакомства с
возможностями программы.

Давайте создадим новый проект (команда
главного меню Файл > Новый).
Щелчком правой кнопки мыши в окне
проекта открывается контекстное
меню. Подменю Insert (по-русски
в главном меню это подменю
называется Достать)
содержит команды добавления
модулей (рис. 1).


Рис. 1. Добавление модуля

Для начала добавим модуль Insert
> MIDI > MIDI IN
. Нетрудно
догадаться, что это входной порт,
через который синтезатор будет
получать команды от приложения-хоста.
На время отладки можно
задействовать виртуальную MIDI-клавиатуру
(модуль Insert > Controls > Keyboard).
Для вывода звука будем
использовать модуль Insert >
Input/Output > Sound Out
. Если бы мы
проектировали эффект или обработку,
то нам потребовался бы модуль Insert
> Input/Output > Sound In
для ввода
звука в будущий плагин.

Все элементы синтезатора нужно
разместить в отдельном модуле (контейнере)
Insert > Container. Вообще,
многие модули по своей сути
являются контейнерами, которые
внутри себя содержат сложную
конструкцию из более простых
элементов.

После добавления перечисленных
модулей окно проекта будет иметь
примерно такой вид, какой показан
на рис. 2, а. Теперь все модули
входных/выходных портов нужно
соединить с модулем контейнера.
Начнем с модуля MIDI In:
захватим мышью надпись MIDI Data
и потянем соединительную линию к
надписи Spare модуля
контейнера (рис. 2, б). После
выполнения соединения на модуле
контейнера появится входной порт MIDI
Data
. Порт MIDI Out
клавиатуры Keyboard соединим
с портом MIDI Data модуля
контейнера. Затем захватим надпись Spare
модуля контейнера и протянем
соединение с модулем Sound Out.
Повторим эту же процедуру и
создадим второе аналогичное
соединение (два канала образуют
стереопару). Результат показан на
рис. 2, в.

а
б
в
Рис. 2. Соединение основных модулей

Если возникнет необходимость
удаления ошибочного соединения,
следует щелкнуть на соединительной
линии (она станет толще) и нажать
клавишу .

Соединительные линии бывают трех
цветов:

  • желтовато-коричневый —
    передача MIDI-информации;
  • фиолетовый — передача звуковой
    информации;
  • голубой — передача CV.

CV (Control Voltage) — управляющее
напряжение, способ связи
электромузыкальных устройств и их
блоков в те времена, когда не было
интерфейса MIDI. С помощью уровня
управляющего напряжения можно
передавать информацию о том, какая
клавиша нажата, или сведения о
положении какого-либо регулятора.
Сигналы Gate являются разновидностью
CV и позволяют передавать одно из
двух состояний чего-либо (например,
клавиша нажата/отпущена).

Соединять между собой можно
только однотипные входы и выходы
модулей.

Контекстное меню каждого из
модулей содержит команду Connect,
которая открывает окно Коммутировать
(рис. 3). Оно предназначено для
редактирования соединений
альтернативным (не графическим)
способом.


Рис. 3. Окно Коммутировать

Командой Properties
контекстного меню модуля
открывается окно его свойств. На
рис. 4 показано окно свойств модуля
контейнера. Основные параметры: Title
— название модуля (сюда можно
вписать название синтезатора), Polyphony
— полифония, Skin — скин (оболочка)
с дизайном панели управления.


Рис. 4. Свойства модуля контейнера

Переименуем модуль контейнера в MY
SYNTH
, закроем окно его свойств,
сделаем двойной щелчок на
заголовке модуля. Откроется окно с
содержимым контейнера (рис. 5).


Рис. 5. Открыто окно с содержимым контейнера

По умолчанию контейнер не
содержит ничего, кроме модуля IO
Mod
, в котором собраны все
доступные порты для ввода/вывода
аудио- и MIDI-данных.

В подменю Insert > Synths
контекстного меню окна проекта и
окон контейнеров доступны команды
добавления «готовых»
синтезаторов. Но они опять-таки
слишком сложны для понимания на
начальном этапе освоения программы.

Создадим простейший синтезатор.
Результат (работающая схема
синтезатора) приведен на рис. 6, файл
проекта — my_synth.se1. Не забыли скачать
файл с примерами по адресу http://petelin.ru/soft/se.zip?
А теперь я поясню, как эта схема
была собрана.


Рис. 6. Простейший синтезатор

Добавим осциллятор (Waveform >
Oscillator
). Модуль содержит
множество управляющих входов,
однако мы задействуем не все из них.
Первым делом нужно заставить
осциллятор генерировать звук,
высота которого соответствует
номеру MIDI-клавиши. Однако
осциллятор управляется
посредством CV (управляющего
напряжения), и его невозможно
подключить ко входному MIDI-порту.
Поэтому потребуется добавить
модуль преобразователя MIDI в CV (Insert
> MIDI > Midi to CV
). Выход MIDI Data
модуля IO Mod соединим со
входом MIDI In модуля Midi
to CV
. Выход Pitch (высота
тона) модуля Midi to CV
соединим со входом Pitch
модуля Oscillator. Теперь
высота тона осциллятора будет
управляться по MIDI.

Следующая задача — нужно сделать
так, чтобы сигнал осциллятора
воспроизводился только тогда,
когда MIDI-клавиша нажата. Для этого
нам потребуется модуль модулятора
амплитуды, управляемый CV (Insert
> Modifiers > VCA
). Выход Audio Out
осциллятора подключим ко входу Signal
(модулируемый по амплитуде сигнал)
модуля VCA. А на управляющий
вход Volume (громкость)
модуля VCA подадим сигнал Gate
с выхода преобразователя Midi to CV.
Сигнал Gate имеет высокий
логический уровень тогда, когда MIDI-клавиша
нажата, и низкий, когда MIDI-клавиша
не нажата. В результате сигнал
осциллятора будет проходить через
модуль VCA только тогда,
когда MIDI-клавиша нажата.

Как вы могли заметить, сигнал
осциллятора монофонический. Хорошо
бы его развести на два стереоканала.
Если этого не сделать, то сигнал
синтезатора будет присутствовать
только в одном из стереоканалов,
что не очень удобно. Добавим модуль
панораматора (Insert > Modifiers > Pan).
На его вход Input подадим
сигнал с выхода Output модуля
VCA. Выходы Left Out и Right
Out
модуля Pan подадим
на стереовыход плагина, т. е.
соединим их со входами 1 и 2
модуля IO Mod.

Последний штрих — сделаем
переключатель волновой формы
осциллятора: модуль Insert > Controls
> List Entry
. Его единственный
выход Choice (выбор) соединим
со входом Waveform модуля Oscillator.
При этом модуль List Entry
автоматически переименуется в Waveform.

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

Все готово? Играйте на MIDI-клавиатуре
и слушайте. Выбирать волновую форму
осциллятора можно с помощью модуля Waveform.

Наигрались? Давайте немного
усложним синтезатор: добавим
регуляторы громкости, панорамы,
скважности сигнала осциллятора;
добавим генератор амплитудной
огибающей ADSR и
соответствующие регуляторы,
индикаторы уровня сигнала;
займемся дизайном элементов
управления (конечный результат
показан на рис. 7, файл проекта
my_synth2.se1).


Рис. 7. Модифицированный вариант синтезатора

Добавим генератор огибающей Insert
> Waveform > ADSR
и включим его в
разрыв между модулями Midi to CV
и VCA: выход Gate
модуля Midi to CV соединим со
входом Gate модуля ADSR,
выход Signal Out модуля ASDR
соединим со входом Volume
модуля VCA.

Добавим регулятор для управления
панорамой Insert > Controls > Slider.
Соединим его выход Signal Out
со входом Pan модуля Pan.
Модуль Slider автоматически
переименуется в Pan.

Откроем свойства регулятора Pan
(команда Properties
контекстного меню). В поле Appearance
(внешний вид) выберем Knob (вращающийся
регулятор). Отключим опцию Show
Readout
(показывать числовое
значение параметра). Опция Show
Title On Panel
(отображать элемент
управления на панели VST) пускай
остается включенной. В полях Lo
Value
и Hi Value
автоматически задаются границы
регулировки в вольтах (для
регулятора панорамы — от -5 В до 5 В).

Аналогичным образом создадим
регулятор громкости Volume:
подключим его ко входу Volume
модуля Pan. При добавлении
однотипных элементов в схему
синтезатора удобно пользоваться
буфером обмена: выделите
образцовый элемент, скопируйте его
в буфер (+), вставьте из
буфера его копию (+).

Точно также создадим регуляторы Attack,
Decay, Sustain и Release
для управления генератором ADSR.
Затем добавим регулятор Pulse Width
для управления скважностью сигнала
осциллятора (регулятор будет
работать только для волновой формы Pulse).
Немного модифицируем
переключатель волновой формы
осциллятора: в свойствах этого
модуля в поле Appearance
выберем Rotary Switch (вращающийся
переключатель).

Командой Insert > Controls > Peak Meter
добавим индикаторы уровня сигнала.
Соединим их входы Signal In с
выходами Left Out и Right Out
модуля Pan. Переименуем эти
модули (в поле Title в
свойствах модуля) в L и R
соответственно.

Перейдем к дизайну панели нашего
синтезатора. В основном окне
проекта откроем контекстное меню
контейнера MY SYNTH и выберем
в нем команду Panel edit.
Откроется окно редактора панели
синтезатора. С помощью мыши
расставим элементы управления
синтезатором так, чтобы было удобно
их использовать (рис. 8).


Рис. 8. Редактор панели синтезатора

Проверьте, как звучит синтезатор,
как работают его регуляторы. Если
все в порядке, то можно сохранить
проект в виде VST-плагина. Для этого
воспользуемся командой главного
меню Файл > Сохранить как VST.
Откроется окно Save as VST (рис.
9). Обратите внимание на то, что в
поле 4-х значный код следует указать
уникальный код плагина, который не
должен повторяться для ваших
плагинов.


Рис. 9. Экспорт VST-плагина

Теоретически к синтезатору могут
прилагаться вспомогательные файлы
(например, MIDI-паттерны). Их нужно
перечислить на вкладке Включить в
состав плагина файлы. По умолчанию
плагины сохраняются в папку C:Program
FilesSynthEditVstPlugins. Если этой папки нет,
создайте ее. Все готово для
экспорта — нажимаем кнопку OK.
Если экспорт VST пройдет удачно,
появится сообщение Saved OK и
название DLL-файла. В нашем случае
файл называется my_synth.dll.

Теперь можно проверить готовый
плагин. Скопируйте его в ту папку,
где лежат все ваши остальные VST-плагины.
Откройте приложение-хост (Cubase, SONAR,
Reaper, FL Studio и др.) и подключите плагин
к проекту (рис. 10).


Рис. 10. Панель плагина,
подключенного к проекту SONAR

При детальном изучении нашего
очень простого синтезатора
проявится серьезный недостаток —
невозможность сохранять пресеты.
Решается эта проблема подключением
специального модуля Insert > MIDI
> Patch Select
: его вход MIDI In
соединяете с выходом MIDI Out
модуля IO Mod, а от выхода MIDI
Out
протягиваете соединение к
надписи Spare модуля IO Mod.
В результате MIDI Out модуля
окажется соединенным со входом MIDI
Out
модуля IO Mod. У
контейнера синтезатора появится
дополнительный выход MIDI Out,
который следует соединить с
модулем MIDI Out (который тоже
нужно добавить). Вид главного окна
проекта после этой модификации
показан на рис. 11 (файл my_synth3.se1). В
результате появится возможность
выбирать, редактировать и
сохранять патчи с помощью
редактора панели плагина. Теперь
плагин будет реагировать на MIDI-команды
смены инструмента (Programm Change)
и сам будет посылать подобные
команды через свой выходной MIDI-порт
при смене патча средствами панели.


Рис. 11. Появилась возможность
редактировать и сохранять патчи

Нет предела совершенству.
Улучшать синтезатор можно до
бесконечности. Но статью пора
заканчивать. Если идея создания
собственных VST вас увлекла, то путь
дальнейшего развития может быть
следующим:

  • изучите назначение всех
    доступных модулей (включая те,
    что можно скачать и установить
    дополнительно);
  • научитесь создавать
    собственный дизайн элементов
    управления (можно посмотреть,
    как сделаны готовые скины —
    наборы графических и текстовых
    файлов в папках, расположенных
    внутри папки C:Program FilesSynthEditskins,
    дополнительные скины можно
    найти в Интернет).

И, наконец, самое главное.
Звучание синтезатора во многом
зависит от звучания его
осцилляторов и фильтров, звучание
сэмплера — от алгоритма
интерполяции. Если вас не
устраивает звучание
соответствующих модулей SynthEdit, то
нужно учиться создавать свои
модули с помощью SDK на C++. Но эта
задача очень непростая, т. к. кроме
умения программировать требуются
еще и глубокие знания в области
обработки сигналов. Гораздо проще
разработать дополнительный модуль
для SynthEdit, чем создавать VST «с нуля»,
используя SDK от Steinberg.

Статья была опубликована в
журнале «Звуковые виртуальные
студии» № 5, 2008.

Я хотел бы сделать (или узнать, как сделать) Плагины VST. Для этого есть специальный SDK? как можно получить a .vst вместо a .exe? Кроме того, если вы хотите сделать аудиоустройства для Logic Pro, как это сделать?
Спасибо

5 ответов


начните с этого ссылке в вики, объясняет, что они такое и дает ссылки на sdk.
Вот некоторая информация о deve

как скомпилировать плагин-для создания плагинов VST в C++Builder, сначала вам нужен VST sdk от Steinberg. Он доступен с сайта Yvan Grabit (ссылка находится в верхней части страницы).

следующее, что вам нужно сделать, это создать .def-файл (например: myplugin.DEF.) Это должно содержать по крайней мере следующие строки:

EXPORTS main=_main

компиляторы Borland добавляют подчеркивание к именам функций, и это экспортирует main() функция так, как ожидает хост VST. Для получения дополнительной информации .def-файлы, см. файлы справки C++Builder.

этого, однако, недостаточно. Если вы собираетесь использовать любой элемент VCL (что-либо связанное с формами или компонентами), вы должны позаботиться о том, чтобы ваш плагин не разбил Cubase (или другой хост VST, если на то пошло). Это место как:

  1. включить float.h.
  2. в конструкторе вашего класса эффектов напишите

    _control87(PC_64|MCW_EM,MCW_PC|MCW_EM);
    

это должно сделать трюк.

вот еще несколько полезных сайтов:

http://www.steinberg.net/en/company/developer.html

как написать плагин vst (pdf) via http://www.asktoby.com/#vsttutorial

43

автор: Development 4.0


Я знаю, что это 3 лет, но для всех, кто читает это сейчас: не придерживайтесь формата VST, AU или любого поставщика. Steinberg прекратил поддерживать VST2, и люди испытывают проблемы с портированием своего кода в новые форматы, потому что он слишком привязан к VST2.

учебники обложка создание плагинов, которые работают на Win / Mac, 32/64, все форматы плагинов из той же базы кода.


Я написал HOWTO для разработки VST на C++ с Visual Studio некоторое время назад, в котором подробно описаны шаги, необходимые для создания базового плагина для платформы Windows (версия Mac этой статьи будет опубликована). В Windows плагин VST — это просто обычная DLL, но есть несколько «gotchas», и вам нужно построить плагин, используя некоторые конкретные коммутаторы компилятора/компоновщика, иначе он не будет распознан некоторыми хостами.

Что касается Mac, плагин VST просто пачка с собой .расширение vst, хотя есть также несколько настроек, которые должны быть настроены правильно для создания допустимого плагина. Вы также можете скачать набор Xcode VST плагин шаблоны проектов Я сделал некоторое время назад, который может помочь вам написать плагин работает на этой платформе.

Что касается AudioUnits, Apple предоставила свои собственные шаблоны проектов, которые включены в Xcode. Apple также имеет очень хорошие учебники и документация online:

  • Руководство По Программированию Аудиоблоков

Я также настоятельно рекомендую проверить Рамки Juce, который имеет отличную поддержку для создания кросс-платформенных плагинов VST/AU. Если вы собираетесь с открытым исходным кодом, то Juce-это без проблем, но вам нужно будет заплатить лицензионные сборы за него, если вы планируете выпустить свою работу без исходного кода.


Если вы знаете язык .NET (C# / VB.NET etc), то проверка VST.NET. Эта структура позволяет создавать (неуправляемые) Плагины VST 2.4 .Сеть. Он поставляется с платформой, которая структурирует и упрощает создание плагина VST с поддержкой параметров, программ и сохраняемости.

существует несколько примеров, демонстрирующих типичные сценарии плагинов. Существует также документация, которая объясняет, как начать работу и некоторые из концепций VST.NET.

надеюсь, что это помогает.
Марк Якоби!—3—>


Я понимаю, что это очень старый пост, но у меня был успех с помощью библиотеки JUCE, которая строит проекты для основных IDE, таких как Xcode, VS и Codeblocks, и автоматически создает VST/3, AU/v3, RTAS и AAX.

Home


Всем привет, сегодня я хотел бы рассказать как написать простейший VST плагин на ассемблере. Те кто создает музыку на компьютере, или занимается обработкой звука хорошо знакомы с этими плагинами и часто используют их как для генерации звука так и для обработки. Основное достоинство таких плагинов — это простота подключения к большинству аудио или музыкальных редакторов. Существуют два типа плагинов VST эффекты и VST инструменты (которые также называют VSTi). В данной статье мы рассмотрим создание VST эффекта, на основе стандарта VST 2.4 который поддерживают большинство редакторов. Программировать будем на FASM’е.
Итак, для начала нужно определится с самим эффектом и для простоты я решил использовать биткрашер. Суть эффекта состоит в понижении разрешения звука как по частоте так и по амплитуде без всякой фильтрации, что дает характерное звучание из-за шумов квантования. Такой эффект нередко можно встретить в электронной музыке, я и сам его очень часто использую. Наглядно эффект продемонстрирован на рисунке:
Нажмите на изображение для увеличения
Название: demonstr.gif
Просмотров: 436
Размер:	223.7 Кб
ID:	4030
Для начала определимся с параметрами эффекта — это частота среза, количество уровней громкости (битность) и дополнительно добавим регулировку выходной громкости. Частота среза у нас может регулироваться от половины частоты дискретизации до нуля, битность от 1 до 16 бит (от 2 до 65536 уровней соответственно), громкость от 0 до 100%. Звук в VST стандарте представляет из себя буффер с семплами, где каждый семпл представлен либо 32-битным числом с плавающей точкой, либо 64 битным числом с плавающей точкой. Частота дискретизации задает количество таких семплов в секунду; максимальная частота которая может быть воспроизведена равна половине частоты дискретизации (обычно 22050Гц). Амплитуда варьируется от -1 до 1, но может также выходить за пределы, что влечет за собой перегруз и клиппинг. Также следует учитывать количество каналов звука, для стерео звука это два канала и каждый обрабатывается независимо. Для того чтобы понизить разрядность звука нужно применить простую формулу:

newValue = int(oldValue * levels) / levels

В итоге из-за округления мы получим дискретный шаг который зависит от величины levels. С ограничением частоты также все просто, для этого найдем сначала количество семплов которое следует пропустить для получения нужной частоты по формуле:

numSamples = sampleRate / downSamplingFreq / 2

Нужно отметить что это число должно быть вещественное для плавной регулировки. Далее нужно просто завести счетчик семлов и периодически сравнивать его значение с numSamples, если оно больше или равно то следует прогрузить следующий семпл в выходной буфер иначе прогружать семпл из предыдущего такого прогруза. Т.к. мы будем использовать стерео обработку, то нужно иметь 2 независимых канала обработки. Из всего этого можно уже примерно накидать структуру эффекта:

Assembler
1
2
3
4
5
6
7
8
9
struct 
    sampleRate              dd ?                ; // Частота дискретизации
    volume                  dd ?                ; // Громкость 0..1 (0..100%)
    downsampling            dd ?                ; // Частота среза 0..1 (0..SampleRate)
    quantize                dd ?                ; // Битность 0..1 (2 ^ (value * 15 + 1))
    lValue                  dd ?                ; // Текущее значение семпла левого канала
    rValue                  dd ?                ; // Текущее значение семпла правого канала
    sampleCounter           dd ?                ; // Счетчик семплов фильтра
ends

Теперь, если просмотреть VST SDK, то можно увидеть что VST плагин представляет собой обычную DLL которая экспортирует функцию VSTPluginMain или main (Main, MAIN, и т.д.). Хост вызывает эту функцию когда создается новый экземпляр VST эффекта. Эта функция должна при успехе возвратить указатель на объект дескриптора эффекта AEEffect, который имеет следующую структуру:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct AEEffect
    magic                   dd ?                ; // Сигнатура 'VstP'
    dispatcher              dd ?                ; // Процедура диспечеризации
    process                 dd ?
    setParameter            dd ?                ; // Установка параметра
    getParameter            dd ?                ; // Получение параметра
    numPrograms             dd ?
    numParams               dd ?
    numInputs               dd ?                ; // Количество входных каналов
    numOutputs              dd ?                ; // Количество выходных каналов
    flags                   dd ?                ; // Флаги
    resvd1                  dd ?
    resvd2                  dd ?
    initialDelay            dd ?
    realQualities           dd ?
    offQualities            dd ?
    ioRatio                 dd ?                
    object                  dd ?                ; // Указатель на объект эффекта
    user                    dd ?
    uniqueId                dd ?                ; // Уникальный ИД эффекта
    version                 dd ?                ; // Версия эффекта
    processReplacing        dd ?                ; // Процедура обработки звука
    processDoubleReplacing  dd ?
    future                  db 56 dup (?)
ends

Как видно структура содержит множество полей, но нас интересуют только некоторые. Самое важное поле это dispatcher — указатель на функцию которая принимает различные запросы от хоста (чем-то похоже на WindowProc); setParameter/getParameter — задают указатели на функции установки/получения параметров от элементов управления или автоматизации. В numInputs/numOutputs мы задаем количество поддерживаемых каналов, в нашем случае 2. Поле object — содержит указатель на связанный пользовательский объект эффекта, т.е. там мы будем хранить указатель на структуру объекта что мы привели ранее. processReplacing и processDoubleReplacing содержат процедуры обработки звуковых данных для 32-float и 64-double соответственно. Для нашего примера мы будем использовать только 32-float Обработку. Флаги задают некоторые характеристики эффекта, мы будем использовать два значения: effFlagsCanReplacing и effFlagsNoSoundInStop. Первый говорит нам что плагин имеет функцию processReplacing и должен быть всегда установлен в VST 2.4 эффекте, а effFlagsNoSoundInStop что плагин ничего не делает если нет входного звука или там тишина. Итак чтобы связать AEEffect и наш эффект соберем их в одну структуру ASMCrusher которая будет олецетворять наш эффект:

Assembler
1
2
3
4
5
6
7
8
9
10
11
; // Объект эффекта ASMCrusher
struct ASMCrusher
    ae                      AEEffect ?          ; // Базовый интерфейс AEEffect
    sampleRate              dd ?                ; // Частота дискретизации
    volume                  dd ?                ; // Громкость 0..1 (0..100%)
    downsampling            dd ?                ; // Частота среза 0..1 (0..SampleRate)
    quantize                dd ?                ; // Битность 0..1 (2 ^ (value * 15 + 1))
    lValue                  dd ?                ; // Текущее значение семпла левого канала
    rValue                  dd ?                ; // Текущее значение семпла правого канала
    sampleCounter           dd ?                ; // Счетчик семплов фильтра
ends

Итак для начала зададим формат файла, декларации типов и констант, импорт, экспорт и зададим релокации:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
format PE GUI 4.0 DLL at 11000000h
 
include 'win32wx.inc'
 
; // Базовый интерфейс VST эффекта
struct AEEffect
    magic                   dd ?                ; // Сигнатура 'VstP'
    dispatcher              dd ?                ; // Процедура диспечеризации
    process                 dd ?
    setParameter            dd ?                ; // Установка параметра
    getParameter            dd ?                ; // Получение параметра
    numPrograms             dd ?
    numParams               dd ?
    numInputs               dd ?                ; // Количество входных каналов
    numOutputs              dd ?                ; // Количество выходных каналов
    flags                   dd ?                ; // Флаги
    resvd1                  dd ?
    resvd2                  dd ?
    initialDelay            dd ?
    realQualities           dd ?
    offQualities            dd ?
    ioRatio                 dd ?
    object                  dd ?                ; // Указатель на объект эффекта
    user                    dd ?
    uniqueId                dd ?                ; // Уникальный ИД эффекта
    version                 dd ?                ; // Версия эффекта
    processReplacing        dd ?                ; // Процедура обработки звука
    processDoubleReplacing  dd ?
    future                  db 56 dup (?)
ends
 
; // Объект эффекта ASMCrusher
struct ASMCrusher
    ae                      AEEffect ?          ; // Базовый интерфейс AEEffect
    sampleRate              dd ?                ; // Частота дискретизации
    volume                  dd ?                ; // Громкость 0..1 (0..100%)
    downsampling            dd ?                ; // Частота среза 0..1 (0..SampleRate)
    quantize                dd ?                ; // Битность 0..1 (2 ^ (value * 15 + 1))
    lValue                  dd ?                ; // Текущее значение семпла левого канала
    rValue                  dd ?                ; // Текущее значение семпла правого канала
    sampleCounter           dd ?                ; // Счетчик семплов фильтра
ends
 
NUMBER_OF_PARAMETERS    = 3                     ; // Количество параметров эффекта
UNIQUE_ID               = 1234567               ; // Уникальный ИД эффекта
VERSION                 = 1                     ; // Версия эффекта
PAR_VOLUME              = 0                     ; // Индексы параметров ...
PAR_DOWNSAMPLING        = 1
PAR_QUANTIZE            = 2
 
kEffectMagic            = 0x56737450            ; // Сигнатура AEEffect
audioMasterVersion      = 1                     ; // Версия хоста
 
; // Максимальные размеры строк
kVstMaxParamStrLen      = 8
kVstMaxVendorStrLen     = 64
kVstMaxProductStrLen    = 64
kVstMaxEffectNameLen    = 32
 
effClose                = 1                     ; // Событие вызывается когда эффект уничтожается
effSetSampleRate        = 10                    ; // Событие установки частоты дискретизации
effGetParamName         = 8                     ; // Событие получения имени параметра
effGetParamLabel        = 6                     ; // Событие получения метки параметра
effGetParamDisplay      = 7                     ; // Событие получения метки значения параметра
effGetEffectName        = 45                    ; // Событие получения имени эффекта
effGetVendorString      = 47                    ; // Событие получения имени производителя
effGetProductString     = 48                    ; // Событие получения имени продукта
effGetVendorVersion     = 49                    ; // Событие получения версии
 
effFlagsCanReplacing    = 16
effFlagsNoSoundInStop   = 512
 
section '.idata' import data readable writeable
 
library kernel, 'kernel32.dll', 
        msvcrt, 'msvcrt.dll'
 
import kernel,
       GetProcessHeap, 'GetProcessHeap', 
       HeapAlloc, 'HeapAlloc', 
       HeapFree, 'HeapFree', 
       lstrcpynA, 'lstrcpynA'
 
import msvcrt, 
       sprintf, 'sprintf'
 
data export
export 'AsmCrusher.DLL', Main, 'Main'
end data
 
section '.reloc' data readable discardable fixups

Одной замечательной особенностью VST стандарта является то что можно вообще не реализовывать пользовательский интерфейс, нужно лишь сообщить хосту количество параметров и их свойства и каждый хост сам предоставит нужные регуляторы и свяжет их с параметрами эффекта. Поэтому далее задаем таблицу строк и список указателей на необходимые строки для каждого параметра, а также точку входа DLL. Таблицу разместим в секции .text:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
section '.text' code readable executable
 
EFFECT_NAME      db      'ASMCrusher', 0
VENDOR_NAME      db      'Кривоус Анатолий Анатольевич (The trick)', 0
PRODUCT_NAME     db      'ASMCrusher', 0
PARAM_NAME_1     db      'Volume', 0
PARAM_NAME_2     db      'Frequency', 0
PARAM_NAME_3     db      'Quantize', 0
PARAM_LABEL_1    db      '%', 0
PARAM_LABEL_2    db      'Hz', 0
PARAM_LABEL_3    db      'Levels', 0
PARAM_FORMAT_1   db      '%d%%', 0
PARAM_FORMAT_2   db      '%dHz', 0
PARAM_FORMAT_3   db      '%d', 0
 
PARAMS_LIST      dd      PARAM_NAME_1, PARAM_NAME_2, PARAM_NAME_3       ; // Имена параметров
LABELS_LIST      dd      PARAM_LABEL_1, PARAM_LABEL_2, PARAM_LABEL_3    ; // Метки единиц измерения параметров
FORMATS_LIST     dd      PARAM_FORMAT_1, PARAM_FORMAT_2, PARAM_FORMAT_3 ; // Форматы параметров
 
entry EntryPoint
 
; // Точка входа DLL
proc EntryPoint, hinstDLL, fdwReason, lpvReserved
    mov eax, 1
    ret
endp

В PARAMS_LIST мы храним указатели на строки имен параметров, в LABELS_LIST на соответствующие единицы измерений для них, а в FORMATS_LIST строки формата для функции sprintf. Каждый экземпляр объекта мы будем хранить в куче процесса, для выделения и освобождения памяти в ней создадим две процедуры:

Assembler
1
2
3
4
5
6
7
8
9
10
11
; // Выделить память
proc MemAlloc, size
    invoke HeapAlloc, <invoke GetProcessHeap>, HEAP_NO_SERIALIZE OR HEAP_ZERO_MEMORY, [size]
    ret
endp
 
; // Освободить память
proc MemFree, pMem
    invoke HeapFree, <invoke GetProcessHeap>, [pMem], HEAP_NO_SERIALIZE
    ret
endp

Теперь можно приступать к непосредственно к реализации стандартных функций VST формата. Первая самая важная функция которую мы также будем экспортировать из DLL будет Main. В ней мы сначала проверяем версию VST хоста, и если она не равна нулю то переходим к созданию эффекта. Создание эффекта — это просто выделение памяти под структуру ASMCrusher и заполнение некоторых ее полей, а также установка свойств по умолчанию:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
; // Вызывается при создании нового экземпляра VST эффекта
proc Main c audioMaster
 
    ; // Проверяем версию
    cinvoke audioMaster, 0, audioMasterVersion, 0, 0, 0, 0
    .if eax = 0
        ret
    .endif
 
    stdcall CreateASMCrusher
 
    ret
 
endp
 
; // Создать объект ASMCrusher
proc CreateASMCrusher uses ebx
 
    stdcall MemAlloc, sizeof.ASMCrusher
 
    .if eax = 0
        ret
    .endif
 
    mov ebx, eax
    lea eax, [ebx + ASMCrusher.ae]
 
    mov [eax + ASMCrusher.ae.magic], kEffectMagic
    mov [eax + ASMCrusher.ae.dispatcher], Dispatcher
    mov [eax + ASMCrusher.ae.setParameter], SetParameter
    mov [eax + ASMCrusher.ae.getParameter], GetParameter
    mov [eax + ASMCrusher.ae.processReplacing], ProcessReplacing
    mov [eax + ASMCrusher.ae.numInputs], 2
    mov [eax + ASMCrusher.ae.numOutputs], 2
    mov [eax + ASMCrusher.ae.numParams], NUMBER_OF_PARAMETERS
    mov [eax + ASMCrusher.ae.flags], effFlagsCanReplacing OR effFlagsNoSoundInStop
    mov [eax + ASMCrusher.ae.uniqueId], UNIQUE_ID
    mov [eax + ASMCrusher.ae.version], VERSION
    mov [eax + ASMCrusher.ae.object], ebx
 
    ; // Загрузка значений по умолчанию
    mov [eax + ASMCrusher.sampleRate], 44100
    mov [eax + ASMCrusher.volume], 1.0
    mov [eax + ASMCrusher.downsampling], 1.0
    mov [eax + ASMCrusher.quantize], 1.0
    mov [eax + ASMCrusher.lValue], 0.0
    mov [eax + ASMCrusher.rValue], 0.0
    mov [eax + ASMCrusher.sampleCounter], 0.0
 
    mov eax, ebx
 
    ret
 
endp

В качестве параметра функция Main принимает указатель на функцию обратного вызова audioMaster, которую мы вызываем для того чтобы определить версию хоста. При создании объекта сначала выделяется память и заполняется члены базового интерфейса AEEffect, затем заполняются поля значений по умолчанию. Dispatcher, SetParameter, GetParameter, ProcessReplacing являются указателями на функции которые будут рассмотрены далее. Следующей важной функцией является функция диспетчеризации — Dispatcher, которая принимает различные события от хоста:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
; // Процедура диспетчеризации
proc Dispatcher c, pEffect, uOpcode, uIndex, value, lpPtr, opt
 
    mov ecx, [pEffect]
    mov ecx, [ecx + AEEffect.object]
 
    .if [uOpcode] = effClose
        ; // Удалить VST эффект
        stdcall MemFree, ecx
        xor eax, eax
    .elseif [uOpcode] = effSetSampleRate
        ; // Установить частоту дискретизации
        mov eax, [opt]
        mov [ecx + ASMCrusher.sampleRate], eax
        xor eax, eax
    .elseif [uOpcode] = effGetParamName
        ; // Получить имя параметра
        mov eax, [uIndex]
        invoke lstrcpynA, [lpPtr], [PARAMS_LIST + eax * 4], kVstMaxParamStrLen
        xor eax, eax
    .elseif [uOpcode] = effGetParamLabel
        ; // Получить имя параметра в окне (надпись)
        mov eax, [uIndex]
        invoke lstrcpynA, [lpPtr], [PARAMS_LIST + eax * 4], kVstMaxParamStrLen
        xor eax, eax
    .elseif [uOpcode] = effGetEffectName
        ; // Получить имя эффекта
        invoke lstrcpynA, [lpPtr], EFFECT_NAME, kVstMaxEffectNameLen
        xor eax, eax
    .elseif [uOpcode] = effGetVendorString
        ; // Получить имя производителя
        invoke lstrcpynA, [lpPtr], VENDOR_NAME, kVstMaxVendorStrLen
        xor eax, eax
    .elseif [uOpcode] = effGetProductString
        ; // Получить имя продукта
        invoke lstrcpynA, [lpPtr], PRODUCT_NAME, kVstMaxProductStrLen
        xor eax, eax
    .elseif [uOpcode] = effGetVendorVersion
        ; // Получить версию
        mov eax, VERSION
    .elseif [uOpcode] = effGetParamDisplay
        ; // Получить значение параметра (надпись)
        .if [uIndex] = PAR_VOLUME
 
            ; // volume * 100
            mov eax, 100.0
            movd xmm0, eax
            mulss xmm0, [ecx + ASMCrusher.volume]
            cvtss2si eax, xmm0
 
            cinvoke sprintf, [lpPtr], [FORMATS_LIST + PAR_VOLUME * 4], eax
 
        .elseif [uIndex] = PAR_DOWNSAMPLING
 
            stdcall CalcDownsamplingFreq, [ecx + ASMCrusher.sampleRate], [ecx + ASMCrusher.downsampling]
            cinvoke sprintf, [lpPtr], [FORMATS_LIST + PAR_DOWNSAMPLING * 4], eax
 
        .elseif [uIndex] = PAR_QUANTIZE
 
            stdcall CalcLevels, [ecx + ASMCrusher.quantize]
            cinvoke sprintf, [lpPtr], [FORMATS_LIST + PAR_QUANTIZE * 4], eax
 
        .endif
 
        xor eax, eax
    .else
        xor eax, eax
    .endif
 
    ret
 
endp

Процедура диспетчеризации принимает несколько параметров, в качестве pEffect передается указатель на AEEffect нашего VST эффекта. В параметре uOpcode передается идентификатор события. uIndex содержит индексный параметр, в нашем случае здесь содержится индекс параметра о котором хост желает получить те или иные сведения. Параметр value и opt содержат целочисленные значения специфичные для события, в параметре lpPtr передается указатель на данные также специфичные для события. Анализируя исходный код видим что процедура состоит из большого switch в котором перебираются идентификаторы события. При получении события effClose мы просто освобождаем память выделенную для нашего объекта. При получении события effSetSampleRate мы устанавливаем частоту дискретизации, которая используется в расчетах; параметр opt содержит float значение частоты дискретизации. События effGetParamName и effGetParamLabel извлекают данные из таблицы строк и записывают данные в выходной параметр lpPtr. Стоит отметить что длина строки ограничена kVstMaxParamStrLen символами. Аналогично effGetEffectName, effGetVendorString, effGetProductString извлекают соответствующие данные из таблиц строк. effGetVendorVersion просто возвращает версию. При получении события effGetParamDisplay мы уже анализируем индекс эффекта, для того чтобы привести значения из логического диапазона 0..1 в реальный текстового вида, который используется в качестве надписи на элементах управления VST. Если это регулятор громкости то мы просто умножаем это число на 100 и добавляем знак процента; если это частота то мы вызываем функцию CalcDownsamplingFreq которая преобразует частоту из диапазона 0..1 в диапазон 0Гц..SampleRate/2, далее формируется строка с добавлением смволов Hz; наконец если это регулятор квантования то вызывается функция CalcLevels которая возвращает количество уровней исходя из диапазона 0..1 (2..65536). Давайте рассмотрим исходный код этих функций:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
; // Получить реальную частоту ресемплинга на основании значения downsampling
; // Вычисляем по формуле int(downsampling * samplerate * 0.5)
proc CalcDownsamplingFreq, sampleRate, downsampling
 
    mov eax, 0.5
    movd xmm0, eax
    mulss xmm0, [downsampling]
    mulss xmm0, [sampleRate]
    cvtss2si eax, xmm0
 
    ret
 
endp
 
; // Посчитать количество уровней сигнала на основании значения quantize
; // Вычисляем по формуле int(2 ^ (quantize * 15 + 1)))
proc CalcLevels, quantize
 
    mov eax, 2
    mov ecx, 15.0
    movss xmm0, [quantize]
    movd xmm1, ecx
    mulss xmm0, xmm1
    cvtss2si ecx, xmm0
    shl eax, cl
 
    ret
 
endp

Первая функция вычисляет частоту по формуле int(downsampling * samplerate * 0.5), где downsampling находится в диапазоне [0..1]. Вторая функция получает количество уровней сигнала по формуле int(2quantize * 15 + 1), где quantize также располагается в диапазоне [0..1]. Эта функция оперирует 16 битными значениями, т.е. максимум получается 65536, а минимум 2. Далее рассмотрим функцию установки и получения параметров:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
; // Получить параметр
proc GetParameter c, pEffect, uIndex
 
    mov ecx, [pEffect]
    mov ecx, [ecx + AEEffect.object]
 
    .if [uIndex] = PAR_VOLUME
        fld [ecx + ASMCrusher.volume]
    .elseif [uIndex] = PAR_DOWNSAMPLING
        fld [ecx + ASMCrusher.downsampling]
    .elseif [uIndex] = PAR_QUANTIZE
        fld [ecx + ASMCrusher.quantize]
    .else
        fldz
    .endif
 
    ret
endp
 
; // Установить параметр
proc SetParameter c, pEffect, uIndex, fValue
 
    mov ecx, [pEffect]
    mov ecx, [ecx + AEEffect.object]
    mov eax, dword [fValue]
 
    .if [uIndex] = PAR_VOLUME
        mov [ecx + ASMCrusher.volume], eax
    .elseif [uIndex] = PAR_DOWNSAMPLING
        mov [ecx + ASMCrusher.downsampling], eax
    .elseif [uIndex] = PAR_QUANTIZE
        mov [ecx + ASMCrusher.quantize], eax
    .endif
 
    ret
 
endp

Каждый параметр в VST кодируется 32 bit — float значением в диапазоне от 0 до 1. Здесь все просто, нужно только отметить что возвращаемое значение возвращается на вершине стека FPU.
При обработке звука вызывается функция ProcessReplacing которая принимает указатель на объект, два указателя на указатели семплов и количество семплов:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
proc ProcessReplacing c uses esi edi ebx, pEffect, pInputs, pOutputs, sampleFrames
 
    mov esi, [pInputs]
    mov edi, [pOutputs]
    mov ebx, [pEffect]
    mov ebx, [ebx + AEEffect.object]
 
    ; // Обрабатываем левый канал
    stdcall ApplyEffectToChannel, ebx, dword [esi], dword [edi], dword [sampleFrames], dword [ebx + ASMCrusher.lValue]
    mov [ebx + ASMCrusher.lValue], eax
 
    ; // Обрабатываем правый канал
    stdcall ApplyEffectToChannel, ebx, dword [esi + 4], dword [edi + 4], dword [sampleFrames], dword [ebx + ASMCrusher.rValue]
    mov [ebx + ASMCrusher.rValue], eax
 
    ret
 
endp

pInputs в нашем случае содержит указатель на два указателя (правый и левый каналы) на звуковые семплы в формате 32 bit float, pOutputs — тоже самое только на выходной буфер. sampleFrames содержит количество семплов в канале. В качестве процедуры обработки служит процедура ApplyEffectToChannel:

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
; // Применить эффект к буферу
; // Возвращает значение семпла
proc ApplyEffectToChannel uses esi edi ebx, pObject, pInput, pOutput, nCount, fValue
 
    mov ebx, [pObject]
    mov esi, [pInput]
    mov edi, [pOutput]
 
    ; // Вычисляем количество уровней
    stdcall CalcLevels, [ebx + ASMCrusher.quantize]
    cvtsi2ss xmm1, eax
 
    ; // Вычисляем количество семплов для частоты среза
    mov eax, 2.0
    movd xmm0, eax
    divss xmm0, [ebx + ASMCrusher.downsampling]
 
    ; // Восстанавливаем регистр счетчика фильтра
    movss xmm2, [ebx + ASMCrusher.sampleCounter]
 
    ; // Загружаем граничные значения
    mov eax, 1.0
    movd xmm3, eax
    mov eax, -1.0
    movd xmm4, eax
 
    ; // Загружаем значение громкости в регистр
    movss xmm6, [ebx + ASMCrusher.volume]
 
    ; // Загружаем сохраненное значение семпла и применяем уровень громкости
    movd xmm5, [fValue]
    mulss xmm5, xmm6
 
    ; // Задаем количество семплов
    mov ecx, [nCount]
 
    ; // Проход по семплам
    .PROCESS_SAMLE:
 
        ; // Увеличиваем счетчик регистра фильтра
        addss xmm2, xmm3
        comiss xmm2, xmm0
        ; // Если количество семлов превышает порог, загружаем новый
        jb .STORE_SAMPLE
 
        ; // Сравниваем с 1
        comiss xmm3, dword [esi]
        jb .SET_MAX
        ; // Сравниваем с -1
        comiss xmm4, dword [esi]
        ja .SET_MIN
        movss xmm5, dword [esi]
 
        .CALC_SAMPLE:
        ; // Сохраняем семп в регистр edx
        movd edx, xmm5
        ; // Вычисляем семпл по формуле int(sample * levels) / levels
        mulss xmm5, xmm1
        cvtss2si eax, xmm5
        cvtsi2ss xmm5, eax
        divss xmm5, xmm1
 
        ; // Изменяем громкость
        mulss xmm5, xmm6
        ; // Обновляем downsampling регистр
        subss xmm2, xmm0
 
        jmp .STORE_SAMPLE
 
        .SET_MAX:
        movss xmm5, xmm3
        jmp .CALC_SAMPLE
        .SET_MIN:
        movss xmm5, xmm4
        jmp .CALC_SAMPLE
 
        .STORE_SAMPLE:
 
        ; // Сохраняем текущий семпл
        movss dword [edi], xmm5
        add edi, 4
        add esi, 4
 
    loop .PROCESS_SAMLE
 
    ; // Сохраняем значения
    movd [ebx + ASMCrusher.sampleCounter], xmm2
 
    ; // Возвращаем значение семпла
    mov eax, edx
 
    ret
 
endp

Эта процедура работает по алгоритмам описаным выше. Стоит отметить что для ускорения большинство действий выполняются в регистрах, на выходе тоолько значения сохраняются в объект для последующего восстановления состояния. Регистр xmm0 содержит количество семплов которые необходимо повторять (удержать) чтобы получить необходимую частоту среза. xmm1 содержит количество уровней квантования. xmm3 и xmm4 содержат константы 1 и -1 которые нужны для проверки выхода за диапазон допустимых значений. xmm6 содержит текуще значение громкости, xmm5 содержит текущее значение семпла умноженное на громкость. xmm2 — счетчик семплов. edx содержит текущее значение семпла без применения умножения громкости. Остальное все понятно из кода и пиведенного в начале описания алгоритма.
Все, пробуем компилировать, и если все выполнено без ошибок в папке с исходником появится DLL. Эту DLL можно теперь подключать к любому хосту. Здесь я приведу несколько примеров GUI хостов:
Нажмите на изображение для увеличения
Название: test.png
Просмотров: 639
Размер:	39.3 Кб
ID:	4031
Исходник прикреплен к сообщению. Всем спасибо за внимание!
С уважением,
Кривоус Анатолий (The trick).

Понравилась статья? Поделить с друзьями:
  • Как написать свой vpn на python
  • Как написать свой scanner java
  • Как написать свой map js
  • Как написать свой lua скрипт для самп
  • Как написать свой exe файл