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




Оглавление

 

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 SDK\pluginterfaces\vst2.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 и т.д.:

 

IVstPluginAudioProcessorIVstPluginParametersIVstPluginProgramsIVstHostAutomationIVstMidiProcessor

 

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

 

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

 


Моя надстройка над 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.

 

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

 

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

 

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

 




Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2019-12-19 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: