- PVSM.RU - https://www.pvsm.ru -
Занимаясь музыкальным творчеством, я часто делаю аранжировки и записи на компьютере — используя кучу всяких VST плагинов и инструментов [1]. Стыдно признаться — я никогда не понимал, как "накручивают" звуки в синтезаторах. Программирование позволило мне написать свой синтезатор, "пропустить через себя" процесс создания звука.
Я планирую несколько статей, в которых будет пошагово рассказано, как написать свой VST плагин/инструмент: программирование осциллятора, частотного фильтра, различных эффектов и модуляции параметров. Упор будет сделан на практику, объяснение программисту простым языком, как же все это работает. Теорию (суровые выводы и доказательства) обойдем стороной (естественно, будут ссылки на статьи и книги).
Обычно плагины пишутся на C++ (кроссплатформенность, возможность эффективно реализовать алгоритмы), но я решил выбрать более подходящий для меня язык — C#; сфокусироваться на изучении самого синтезатора, алгоритмов, а не технических деталей программирования. Для создания красивого интерфейса я использовал WPF. Возможность использования архитектуры .NET дала возможность библиотека-обертка VST. NET [2].
Ниже представлен обзорный ролик моего простого синтезатора [3], полученных интересных звучаний.
Предстоит нелегкий путь, если вы готовы — добро пожаловать под кат.
Загадочный мир синтеза звука [4]
Звук в цифровом виде [5]
VST SDK [6]
WDL-OL и JUCE [7]
VST .NET [8]
Моя надстройка над VST .NET [9]
WPF UI [10]
UI-поток [11]
Обзор архитектуры синтезатора Syntage [12]
Настраиваем проект для создания плагина/инструмента [13]
Отладка кода [14]
Пишем простой осциллятор [15]
Список литературы [16]
Я очень люблю музыку, слушаю разные стили, играю на различных инструментах, и, конечно, сочиняю и записываю аранжировки. Когда я начинал использовать эмуляторы синтезаторов в звукозаписывающих программах (да и сейчас) я всегда перебирал кучу пресетов, искал подходящее звучание.
Перебирая пресеты одного синтезатора можно встретить как "ожидаемый" звук электронного синтезатора из детства (музыка из мультика Летучий Корабль [17]) так и имитацию ударных, звуков, шума, даже голоса! И все это делает один синтезатор, с одними и теми же ручками параметров. Это меня всегда удивляло, хотя я понимал: каждый звук — суть конкретная настройка всех ручек.
Недавно я решил наконец-таки разобраться, каким же образом создаётся (или, правильнее сказать, синтезируется) звук, как и почему нужно крутить ручки, как видоизменяется от эффектов сигнал (визуально и на слух). И конечно же, научиться (хотя бы понять основы) самому "накручивать" звук, копировать понравившиеся мне стили. Я решил последовать одной цитате:
"Скажи мне — и я забуду, покажи мне — и я запомню, дай мне сделать — и я пойму."
Конфуций
Конечно, все подряд делать не надо (куда столько велосипедов?), но сейчас я хочу получить знания и самое главное — поделиться ими с вами.
Цель: не углубляясь в теорию, создать простой синтезатор, сделав упор на объяснение процессов с точки зрения программирования, на практике.
В синтезаторе будут:
Все составляющие я планирую рассмотреть в нескольких статьях. В данной будет рассмотрено программирование осциллятора.
Программировать будем на C#; UI можно писать либо на WPF, либо на Windows Forms, либо вообще обойтись без графической оболочки. Плюс выбора WPF — красивая графика, которую достаточно быстро кодить, минус — только на Windows. Владельцы других ОС — не расстраивайтесь, всё-таки цель — понять работу синтезатора (а не запилить красивый UI), тем более, код, который я буду демонстрировать, можно быстро перенести, скажем, на С++.
В главах VST SDK [6] и WDL-OL и JUCE [7] я расскажу про концепцию VST, ее внутреннюю реализацию; про библиотеки-надстройки, которые хорошо подойдут для разработки серьезных плагинов. В главе VST .NET [8] я расскажу про данную библиотеку, ее минусы, мою надстройку, программирование UI.
Программирование логики синтезатора начнется с главы Пишем простой осциллятор [15]. Если вам не интересны технические стороны написания VST плагинов, вы просто хотите прочитать про, собственно, синтез (и ничего не кодить) — милости прошу сразу к этой главе.
Исходный код написанного мной синтезатора доступен на GitHub'е [3].
По-сути, конечная наша цель — создание звука на компьютере. Обязательно прочитайте (хотя бы, бегло) статью на хабре "Теория звука" [22] — в ней изложены базовые знания о представлении звука на компьютере, понятия и термины.
Любой звуковой файл в компьютере в несжатом формате представляет собой массив семплов. Любой плагин, в конечном счете, принимает и обрабатывает на входе массив семлов (в зависимости от точности это будут float или double числа, либо можно работать с целыми числами). Почему я сказал массив, а не одиночный семпл? Этим я хотел подчеркнуть что обрабатывается звук в целом: если вам нужно сделать эквализацию [23], вы не сможете оперировать одним лишь семплом без информации о других.
Хотя, конечно, есть задачи, которым не важно знать, что вы обрабатываете — они рассматривают конкретный семпл. Например, задача — поднять уровень громкости в 2 раза. Мы можем работать с каждым семплом по-отдельности, и нам не нужно знать про остальные.
Мы будем работать с семплом как с float-числом от -1 до 1. Обычно, чтобы не говорить "значение семпла", можно сказать "амплитуда". Если амплитуда каких-то семплов будет больше 1 или меньше -1, произойдет клиппинг [24], этого нужно избегать.
VST (Virtual Studio Technology) [1] — это технология, позволяющая писать плагины для программ обработки звука. Сейчас существует большое множество плагинов, решающих различные задачи: синтезаторы, эффекты, анализаторы звука, виртуальные инструменты и так далее.
Чтобы создавать VST плагины, компания Steinberg (некоторые ее знают по программе Cubase) выпустила VST SDK, написанный на C++. Помимо технологии (или, как еще говорят, "формата плагинов") VST, есть и другие — RTAS, AAX, тысячи их [25]. Я выбрал VST, из-за большей известности, большого количества плагинов и инструментов (хотя, большинство известных плагинов поставляется в разных форматах).
На данный момент актуальная версия VST SDK 3.6.6, хотя многие продолжают использовать версию 2.4. Исторически складывается, что сложно найти DAW [26] без поддержки версии 2.4, и не все поддерживают версию 3.0 и выше (например Reaper вообще с версией 3.0 и выше не работает).
VST SDK можно скачать с официального сайта [27].
В дальнейшем мы будем работать с библиотекой VST.NET, которая является оберткой для VST 2.4.
Если вы намерены серьезно разрабатывать плагины, и хотите использовать последнюю версию SDK, то вы можете самостоятельно изучить документацию и примеры (все можно скачать с официального сайта [28]).
Сейчас я кратко изложу принципы VST SDK 2.4, для общего понимания работы плагина и его взаимодействия с DAW [26].
В Windows VST плагин версии 2.4 представляется как динамическая DLL библиотека.
Хостом мы будем называть программу, которая загружает нашу DLL. Обычно это либо программа редактирования музыки (DAW [26]), либо простая оболочка, чтобы запускать плагин независимо от других программ (например, очень часто в виртуальных инструментах с .dll плагином поставляется .exe файл, чтобы загружать плагин как отдельную программу — пианино, синтезатор).
Дальнейшие функции, перечисления и структуры вы можете найти в скачанном VST SDK [28] в исходниках из папки "VST3 SDKpluginterfacesvst2.x".
Библиотека должна экспортировать функцию со следующей сигнатурой:
EXPORT void* VSTPluginMain(audioMasterCallback hostCallback)
Функция принимает указатель на коллбэк, чтобы плагин мог получать необходимую ему информацию от хоста.
VstIntPtr (VSTCALLBACK *audioMasterCallback) (AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt)
Все делается на достаточно "низком" уровне — чтобы хост понял, что от него хотят, нужно передавать номер команды через параметр opcode. Перечисление всех опкодов хардкорные C-кодеры могут найти в перечислении AudioMasterOpcodesX. Остальные параметры используются аналогичным образом.
VSTPluginMain должна вернуть указатель на структуру AEffect, которая, по-сути, и является нашим плагином: она содержит информацию о плагине и указатели на функции, которые будет вызывать хост.
Основные поля структуры AEffect:
Фунция обработки массива семплов
void (VSTCALLBACK *AEffectProcessProc) (AEffect* effect, float** inputs, float** outputs, VstInt32 sampleFrames)
float** — это массив каналов, каждый канал содержит одинаковое количество семплов (количество семплов в массиве зависит от звукового драйвера и его настроек). В основном встречаются плагины, обрабатывающие моно и стерео.
Супер-функция, подобна audioMasterCallback.
VstIntPtr (VSTCALLBACK *AEffectDispatcherProc) (AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt)
Вызывается хостом, по параметру opcode определяется необходимое действие (список AEffectOpcodes). Используется, чтобы узнать дополнительную информацию о параметрах, сообщать плагину об изменениях в хосте (изменение частоты дискредитации), для взаимодействия с UI плагина.
При работе с плагином было бы очень удобно, чтобы юзер мог сохранить все настроенные ручки и переключатели. А еще круче, чтобы была их автоматизация! Например, вы можете захотеть сделать знаменитый эффект rise up [29] — тогда вам нужно менять параметр cutoff (частота среза) [30] эквалайзера во времени.
Чтобы хост управлял параметрами вашего плагина, в AEffect есть соответствующие функции: хост может запросить общее количество параметров, узнать или установить значение конкретного параметра, узнать название параметра, его описание, получить отображаемое значение.
Хосту все равно, какая логика у параметров в плагине. Задача хоста — сохранять, загружать, автоматизировать параметры. Хосту очень удобно воспринимать параметр, как float-число от 0 до 1 — а уж плагин пусть как хочет, так его и толкует (так и сделали большинство DAW, неофициально).
Пресеты (в терминах VST SDK — programs/программы) это коллекция конкретных значений всех параметров плагина. Хост может менять/переключать/выбирать номера пресетов, узнавать их названия, аналогично с параметрами. Банки — коллекция пресетов. Банки логически существуют только в DAW, в VST SDK есть только пресеты и программы.
Поняв идею структуры AEffect можно набросать и скомпилировать простой DLL-плагинчик.
А мы пойдем дальше, на уровень выше.
Чем плоха разработка на голом VST SDK?
На сцену выходит WDL-OL [31]. Это C++ библиотека для создания кроссплатформенных плагинов. Поддерживаются форматы VST, VST3, Audiounit, RTAS, AAX. Удобство библиотеки состоит в том, что (при правильной настройке проекта) вы пишете один код, а при компилировании получаете свой плагин в разных форматах.
Как работать с WDL-OL хорошо описано в Martin Finke's Blog "Music & Programming" [32], даже есть хабр статьи-переводы на русский [33].
WDL-OL решает, по крайней мере, первые три пункта минусов разработки на VST SDK. Все, что вам нужно — корректно настроить проект (первая статья из блога), и отнаследоваться от класса IPlug.
class MySuperPuperPlugin : public IPlug
{
public:
explicit MyFirstPlugin(IPlugItanceInfo instanceInfo);
virtual ~MyFirstPlugin() override;
void ProcessDoubleReplacing(double** inputs, double** outputs, int nFrames) override;
};
Теперь с чистой совестью можно реализовать функцию ProcessDoubleReplacing, которая, по сути и является "ядром" плагина. Все заботы взял на себя класс IPlug. Если его изучать, можно быстро понять, что (в формате VST) он является оберткой структуры AEffect. Коллбэки от хоста и функции для хоста превратились в удобные виртуальные функции, с понятными названиями и адекватными списками параметров.
В WDL-OL уже есть средства для создания UI. Но как по мне, все это делается с большой болью: UI собирается в коде, все ресурсы нужно описывать в .rc файле и так далее.
Помимо WDL-OL я так же узнал про библиотеку JUCE [34]. JUCE похожа на WDL-OL, решает все заявленные минусы разработки на VST SDK. Помимо всего прочего, она уже имеет в своем составе и UI-редактор, и кучу классов для работы с аудио данными. Я лично ее не использовал, поэтому советую прочитать о ней, хотя бы, на вики [35].
Если вы хотите писать серьезный плагин, тут я бы уже всерьез задумался над использованием библиотек WDL-OL или JUCE. Всю рутину они сделают за вас, а у вас же остается вся мощь языка C++ для реализации эффективных алгоритмов и кроссплатформенность — что не маловажно в мире большого количества DAW.
Чем же мне не угодили WDL-OL и JUCE?
Страничка библиотеки — vstnet.codeplex.com [36], там есть исходники, бинарники, документация. Как я понял, библиотека находится в стадии почти доделал и забил заморозки (не реализованы некоторые редко используемые функции, пару лет нет изменений репозитория).
Библиотека состоит из трех ключевых сборок:
Как можно делать .NET сборки, если хост ожидает простую динамическую DLL? А вот как: на самом деле хост грузит не вашу сборку, а скомпилированную DLL Jacobi.Vst.Interop, которая уже в свою очередь грузит ваш плагин в рамках .NET.
Используется следующая хитрость: допустим, вы разрабатываете свой плагин, и на выходе получаете .NET-сборку MyPlugin.dll. Нужно сделать так, чтобы хост вместо вашей MyPlugin.dll загрузил Jacobi.Vst.Interop.dll, а она загрузила ваш плагин. Вопрос, а как Jacobi.Vst.Interop.dll узнает откуда грузить вашу либу? Вариантов решения много. Разработчик выбрал вариант называть либу-обертку одинаковым именем с вашей либой, а затем искать .NET-сборку как "мое_имя.vstdll".
Работает все это следующим образом
При загрузке вашей либы необходимо, чтобы в ней был класс, реализующий интерфейс IVstPluginCommandStub:
public interface IVstPluginCommandStub : IVstPluginCommands24
{
VstPluginInfo GetPluginInfo(IVstHostCommandStub hostCmdStub);
Configuration PluginConfiguration { get; set; }
}
VstPluginInfo содержит базовую о плагине — версия, уникальный ID плагина, число параметров и программ, число обрабатываемых каналов. PluginConfiguration нужна для вызывающей либы-обертки Jacobi.Vst.Interop.
В свою очередь, IVstPluginCommandStub реализует интерфейс IVstPluginCommands24, который содержит методы, вызываемые хостом: обработка массива (буфера) семплов, работа с параметрами, программами (пресетами), MIDI-сообщениями и так далее.
Jacobi.Vst.Framework содержит готовый удобный класс StdPluginCommandStub, реализующий IVstPluginCommandStub. Все что нужно сделать — отнаследоваться от StdPluginCommandStub и реализовать метод CreatePluginInstance(), который будет возвращать объект (instance) вашего класса-плагина, реализующего IVstPlugin.
public class PluginCommandStub : StdPluginCommandStub
{
protected override IVstPlugin CreatePluginInstance()
{
return new MyPluginController();
}
}
Опять же, есть готовый удобный класс VstPluginWithInterfaceManagerBase:
public abstract class VstPluginWithInterfaceManagerBase : PluginInterfaceManagerBase, IVstPlugin, IExtensible, IDisposable
{
protected VstPluginWithInterfaceManagerBase(string name, VstProductInfo productInfo, VstPluginCategory category,
VstPluginCapabilities capabilities, int initialDelay, int pluginID);
public VstPluginCapabilities Capabilities { get; }
public VstPluginCategory Category { get; }
public IVstHost Host { get; }
public int InitialDelay { get; }
public string Name { get; }
public int PluginID { get; }
public VstProductInfo ProductInfo { get; }
public event EventHandler Opened;
public virtual void Open(IVstHost host);
public virtual void Resume();
public virtual void Suspend();
protected override void Dispose(bool disposing);
protected virtual void OnOpened();
}
Если смотреть исходный код библиотеки, можно увидеть интерфейсы, описывающие компоненты плагина, для работы с аудио, параметрами, MIDI и т.д. :
IVstPluginAudioProcessor
IVstPluginParameters
IVstPluginPrograms
IVstHostAutomation
IVstMidiProcessor
Класс VstPluginWithInterfaceManagerBase содержит виртуальные методы, возвращающие эти интерфейсы:
protected virtual IVstPluginAudioPrecisionProcessor CreateAudioPrecisionProcessor(IVstPluginAudioPrecisionProcessor instance);
protected virtual IVstPluginAudioProcessor CreateAudioProcessor(IVstPluginAudioProcessor instance);
protected virtual IVstPluginBypass CreateBypass(IVstPluginBypass instance);
protected virtual IVstPluginConnections CreateConnections(IVstPluginConnections instance);
protected virtual IVstPluginEditor CreateEditor(IVstPluginEditor instance);
protected virtual IVstMidiProcessor CreateMidiProcessor(IVstMidiProcessor instance);
protected virtual IVstPluginMidiPrograms CreateMidiPrograms(IVstPluginMidiPrograms instance);
protected virtual IVstPluginMidiSource CreateMidiSource(IVstPluginMidiSource instance);
protected virtual IVstPluginParameters CreateParameters(IVstPluginParameters instance);
protected virtual IVstPluginPersistence CreatePersistence(IVstPluginPersistence instance);
protected virtual IVstPluginProcess CreateProcess(IVstPluginProcess instance);
protected virtual IVstPluginPrograms CreatePrograms(IVstPluginPrograms instance);
Эти методы и нужно перегружать, чтобы реализовывать свою логику в кастомных классах-компонентах. Например, вы хотите обрабатывать семплы, тогда вам нужно написать класс, реализующий IVstPluginAudioProcessor, и вернуть его в методе CreateAudioProcessor.
public class MyPlugin : VstPluginWithInterfaceManagerBase
{
...
protected override IVstPluginAudioProcessor CreateAudioProcessor(IVstPluginAudioProcessor instance)
{
return new MyAudioProcessor();
}
...
}
...
public class MyAudioProcessor : VstPluginAudioProcessorBase // используем готовый класс из либы
{
public override void Process(VstAudioBuffer[] inChannels, VstAudioBuffer[] outChannels)
{
// обработка семплов
}
}
Используя различные готовые классы-компоненты можно сосредоточиться на программировании логики плагина. Хотя, вам никто не мешает реализовывать все самому, как хочется, основываясь только на интерфейсах из Jacobi.Vst.Core.
Для тех, кто уже кодит — предлагаю вам пример просто плагина, который понижает громкость на 6 дБ (для этого нужно умножить семпл на 0.5, почему — читай в статье про звук [22]).
using Jacobi.Vst.Core;
using Jacobi.Vst.Framework;
using Jacobi.Vst.Framework.Plugin;
namespace Plugin
{
public class PluginCommandStub : StdPluginCommandStub
{
protected override IVstPlugin CreatePluginInstance()
{
return new MyPlugin();
}
}
public class MyPlugin : VstPluginWithInterfaceManagerBase
{
public MyPlugin() : base(
"MyPlugin",
new VstProductInfo("MyPlugin", "My Company", 1000),
VstPluginCategory.Effect,
VstPluginCapabilities.None,
0,
new FourCharacterCode("TEST").ToInt32())
{
}
protected override IVstPluginAudioProcessor CreateAudioProcessor(IVstPluginAudioProcessor instance)
{
return new AudioProcessor();
}
}
public class AudioProcessor : VstPluginAudioProcessorBase
{
public AudioProcessor() : base(2, 2, 0) // плагин будет обрабатывать стерео
{
}
public override void Process(VstAudioBuffer[] inChannels, VstAudioBuffer[] outChannels)
{
for (int i = 0; i < inChannels.Length; ++i)
{
var inChannel = inChannels[i];
var outChannel = outChannels[i];
for (int j = 0; j < inChannel.SampleCount; ++j)
{
outChannel[j] = 0.5f * inChannel[j];
}
}
}
}
}
При программировании синта я столкнулся с некоторыми проблемами при использовании классов из Jacobi.Vst.Framework. Основная проблема заключалась в использовании параметров и их автоматизации.
Во первых, мне не понравилась реализация событий изменения значения; во вторых, обнаружились баги при тестировании плагина в FL Studio и Cubase. FL Studio воспринимает все параметры как float-числа от 0 до 1, даже не используя специальную функцию из VST SDK с опкодом effGetParameterProperties (функция вызывается у плагина чтобы получить дополнительную информацию о параметре). В WDL-OL реализация закомментирована с пометкой:
could implement effGetParameterProperties to group parameters, but can't find a host that supports it
Хотя, конечно же, в Cubase эта функция вызывается (Cubase — продукт компании Steinberg, которая и выпустила VST SDK).
В VST .NET этот коллбэк реализован в виде функции GetParameterProperties, возвращающей объект класса VstParameterProperties. Все равно, Cubase некорректно воспринимал и автоматизировал мои параметры.
В начале я внес правки саму библиотеку, написал автору, чтобы он дал разрешение выложить исходники в репозиторий, либо сам создал репозиторий на GitHub'е. Но внятного ответа я так и не получил, поэтому решил сделать надстройку над либой — Syntage.Framework.dll.
Помимо этого, в надстройке реализованы удобные классы для работы с UI, если вы хотите использовать WPF.
Самое время скачать исходный код моего синтезатора [3] и скомпилировать его.
Правила использования моей надстройки просты: вместо StdPluginCommandStub юзаем SyntagePluginCommandStub, а свой плагин наследуем от SyntagePlugin.
В VST плагине не обязательно должен быть графический интерфейс. Я видел много плагинов без UI (одни из них — mda [40]). Большинство DAW (по крайней мере, Cubase и FL Studio) предоставят вам возможность управлять параметрами из сгенерированного ими UI.
Автосгенерированный UI для моего синтезатора в FL Studio
Чтобы ваш плагин был с UI, во-первых, у вас должен быть класс, реализующий IVstPluginEditor; во-вторых, нужно вернуть его инстанс в перегруженной функции CreateEditor вашего класса плагина (наследник SyntagePlugin).
Я написал класс PluginWpfUI<T>, который непосредственно владеет WPF-окном. Здесь T — это тип вашего UserControl, являющийся "главной формой" UI. PluginWpfUI<T> имеет 3 виртуальных метода, которые вы можете перегружать для реализации своей логики:
В своем синтезаторе Syntage я написал пару контролов — слайдер, крутилка (knob), клавиатура пианино — если вы хотите, можете их скопировать и использовать.
Я тестировал синтезатор в FL Studio и Cubase 5 и уверен, что, в других DAW будет тоже самое: UI плагина обрабатывается отдельным потоком. А это значит, что логики аудио и UI обрабатывается в независимых потоках. Это влечет все проблемы, или, последствия такого подхода: доступ к данным из разных потоков, критические данные, доступ к UI из другого потока...
Для облегчения решения проблем я написал класс UIThread, который, по сути, является очередью команд. Если вы в какой-то момент хотите что-то сообщить/поменять/сделать в UI, а текущий код работает не в UI-потоке, то вы можете поставить на выполнение в очередь необходимую функцию:
UIThread.Instance.InvokeUIAction(() => Control.Oscilloscope.Update());
Здесь в очередь команд помещается анонимный метод, обновляющий нужные данные. При вызове ProcessIdle все накопившиеся в очереди команды будут выполнены.
UIThread не решает всех проблем. При программировании осциллографа [41] необходимо было обновлять UI по массиву семплов, который обрабатывался в другом потоке. Пришлось использовать мьютексы.
При написании синтезатора активно использовалось ООП; предлагаю вам познакомиться с получившейся архитектурой и использовать мой код. Вы можете сделать все по-своему, но в этих статьях придется терпеть мое видение)
Класс PluginCommandStub нужен только чтобы создать и вернуть объект класса PluginController. PluginController предоставляет информацию о плагине, так же создает и владеет следующими компонентами:
Чтобы обрабатывать аудиоданные есть интерфейсы IAudioChannel и IAudioStream. IAudioChannel предоставляет прямой доступ к массиву/буферу семплов (double[] Samples). IAudioStream содержит массив каналов.
Представленные интерфейсы содержат удобные методы обработки всех семплов и каналов "скопом": микширование каналов и потоков, применение метода к каждому семплу в отдельности и так далее.
Для интерфейсов IAudioChannel и IAudioStream написаны реализации AudioChannel и AudioStream. Здесь важно запомнить следующую вещь: нельзя хранить ссылки на AudioStream и AudioChannel, если они являются внешними данными в функции. Суть в том, что размеры буферов могут меняться по ходу работы плагина, буферы постоянно переиспользуются — не выгодно постоянно перевыделять и копировать память. Если вам необходимо сохранить буфер для дальнейшего использования (уж не знаю, зачем) — копируйте его в свой буфер.
IAudioStreamProvider является владельцем аудиопотоков, можно попросить создать поток функцией CreateAudioStream и вернуть поток для его удаления функцией ReleaseAudioStream.
В каждый момент времени длина (длина массива семплов) всех аудиопотоков и каналов одинакова, технически она определяется хостом. В коде ее можно получить либо у самого IAudioChannel или IAudioStream (свойство Length), так же у "хозяина" IAudioStreamProvider (свойство CurrentStreamLenght).
Класс AudioProcessor является "ядром" синтезатора — в нем-то и происходит синтез звука. Класс является наследником SyntageAudioProcessor, который, в свою очередь, реализует следующие интерфейсы:
Синтез звука проходит длинную цепочку обработки: создание простой волны в осцилляторах, микширование звука с разных осцилляторов, последовательная обработка в эффектах. Логика создания и обработки звука была разделена на классы-компоненты для AudioProcessor. Каждый компонент является наследником класса SyntageAudioProcessorComponentWithParameters<T> — содержит ссылку на AudioProcessor и возможность создавать параметры.
В синтезаторе представлены следующие компоненты:
Все этапы создания звука вы можете найти в функции 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 [8]).
Предлагаю вам использовать следующий скрипт (его нужно прописать в Project → Properties → Build Events → Post-build event command line, выполнение скрипта поставьте на On successful build):
if not exist "$(TargetDir)vst" mkdir "$(TargetDir)vst"
copy "$(TargetDir)$(TargetFileName)" "$(TargetDir)vst$(TargetName).net.vstdll"
copy "$(TargetDir)Syntage.Framework.dll" "$(TargetDir)vstSyntage.Framework.dll"
copy "$(TargetDir)Jacobi.Vst.Interop.dll" "$(TargetDir)vst$(TargetName).dll"
copy "$(TargetDir)Jacobi.Vst.Core.dll" "$(TargetDir)vstJacobi.Vst.Core.dll"
copy "$(TargetDir)Jacobi.Vst.Framework.dll" "$(TargetDir)vstJacobi.Vst.Framework.dll"
В файле моего проекта Syntage вы найдете сборку SimplyHost. Это простой хост, который на старте загружает плагин с расширением ".vstdll" (файл ищется рядом с .exe или в дочерних папках). Рекомендую вам скопировать его к себе в проект — тогда вы без проблем сразу сможете отлаживать свой плагин.
Вы так же можете использовать другие хосты для отладки, но сделать это будет уже сложнее. Когда я тестировал синтезатор, я использовал две DAW: FL Studio 12 и Cubase 5. Если в FL Studio загрузить плагин, можно из Visual Studio приконнектиться к процессу FL Studio (Debug → Attach To Process). Это не всегда работает, нужно быть очень внимательным: загружаемая .dll должна соответствовать вашему коду в студии (пересоберите проект перед отладкой); коннектиться к процессу можно только после загрузки вашего плагина в DAW.
Я надеюсь, что вы прочитали главу "Обзор архитектуры синтезатора Syntage" — я буду объяснять все в терминах своей архитектуры.
Самый простой звук — это чистый тон (синусоидальный сигнал, синус) определенной частоты. В природе вы вряд ли сможете услышать чистый тон. В жизни же можно услышать чистые тона в какой-нибудь электронике (и то, уверенности мало). Фурье сказал [44], что любой звук можно представить как одновременное звучание тонов разной частоты и громкости. Окраска звука характеризуется тембром [45] — грубо говоря, описанием соотношения тонов в этом звуке (спектром [46]).
Мы пойдем схожим путем — будем генерировать простой сигнал, а затем воздействовать на него и менять с помощью эффектов.
Какие выбрать "простые" сигналы? Очевидно, сигналы, спектр которых известен и хорошо изучен, которые легко обрабатывать. Возьмем четыре знаменитые типа сигналов:
Периоды четырех типов сигналов: синус, треугольник, импульс/квадрат, пила.
Чтобы синтезировать звуки, вы должны четко представлять себе исходное звучание этих простых сигналов.
Синус имеет глухое и тихое звучание, остальные же — "острое" и громкое. Это связано с тем, что, в отличие от синуса, другие сигналы содержат большое количество других тонов (гармоник) в спектре.
Наш генерируемый сигнал будет характеризоваться двумя параметрами: типом волны и частотой.
На графике изображены периоды нужных нам волн. Заметьте, что все волны представлены в интервале от 0 до 1. Это очень удобно, так как позволяет одинаково запрограммировать расчет значений. Такой подход позволяет задать произвольную форму сигнала, я даже видел синтезаторы, где можно вручную его нарисовать.
По представленным картинкам напишем вспомогательный класс WaveGenerator, с методом GetTableSample, который будет возвращать значение амплитуды сигнала в зависимости от типа волны и времени (время должно быть в пределах от 0 до 1).
Добавим так же в тип волны белый шум [47] — он полезен в синтезе нестандартных звуков. Белый шум характеризуется тем, что спектральные составляющие равномерно распределены по всему диапазону частот. Функция NextDouble стандартного класса Random имеет равномерное распределение — таким образом, мы можем считать, что каждый сгенерированный семпл относится к некоторой гармонике. Соответственно, мы будем выбирать гармоники равномерно, получая белый шум. Нужно лишь сделать отображение результата функции из интервала [0,1] в интервал минимального и максимального значения амплитуды [-1,1].
public static class WaveGenerator
{
public enum EOscillatorType
{
Sine,
Triangle,
Square,
Saw,
Noise
}
private static readonly Random _random = new Random();
public static double GetTableSample(EOscillatorType oscillatorType, double t)
{
switch (oscillatorType)
{
case EOscillatorType.Sine:
return Math.Sin(DSPFunctions.Pi2 * t);
case EOscillatorType.Triangle:
if (t < 0.25) return 4 * t;
if (t < 0.75) return 2 - 4 * t;
return 4 * (t - 1);
case EOscillatorType.Square:
return (t < 0.5f) ? 1 : -1;
case EOscillatorType.Saw:
return 2 * t - 1;
case EOscillatorType.Noise:
return _random.NextDouble() * 2 - 1;
default:
throw new ArgumentOutOfRangeException();
}
}
}
Теперь, пишем класс Oscillator, который будет наследником SyntageAudioProcessorComponentWithParameters<AudioProcessor>. В осцилляторе рождается звук, поэтому класс будет реализовывать интерфейс IGenerator, а именно функцию
IAudioStream Generate();
Необходимо запросить у IAudioStreamProvider (для нас это будет родительский AudioProcessor) аудиопоток, и в каждом вызове функции Generate заполнять его сгенерированными семплами.
Пока что у нашего осциллятора будет два параметра:
Оформим все вышесказанное:
public class Oscillator : SyntageAudioProcessorComponentWithParameters<AudioProcessor>, IGenerator
{
private readonly IAudioStream _stream; // поток, куда будем генерировать семплы
private double _time;
public EnumParameter<WaveGenerator.EOscillatorType> OscillatorType { get; private set; }
public RealParameter Frequency { get; private set; }
public Oscillator(AudioProcessor audioProcessor) :
base(audioProcessor)
{
_stream = Processor.CreateAudioStream(); // запрашиваем поток
}
public override IEnumerable<Parameter> CreateParameters(string parameterPrefix)
{
OscillatorType = new EnumParameter<WaveGenerator.EOscillatorType>(parameterPrefix + "Osc", "Oscillator Type", "Osc", false);
Frequency = new FrequencyParameter(parameterPrefix + "Frq", "Oscillator Frequency", "Hz");
return new List<Parameter> { OscillatorType, Frequency };
}
public IAudioStream Generate()
{
_stream.Clear(); // очищаем все, что было раньше
GenerateToneToStream(); // самое интересное
return _stream;
}
}
Осталось написать функцию GenerateToneToStream.
Каждый раз когда мы будем генерировать семплы сигнала, мы должны помнить о двух значениях:
Оба параметра могут меняться во время работы плагина, поэтому не советую каким-либо образом их кешировать. Каждый вызов функции Generate() на вход плагину подается буфер конечной длины (длина определяется хостом, по времени она достаточно короткая) — звук генерируется "порциями". Мы должны запоминать, сколько времени прошло с момента начала генерирования волны, чтобы звук был "непрерывным". Пока что звук будем генерировать с момента старта плагина. Синхронизировать звук с нажатием клавиши будем в следующей статье.
Семплы генерируются в цикле от 0 до [длина текущего буфера].
Частота дискретизации — число семплов в секунду. Время, которое проходит от начала одного семпла до другого равно timeDelta = 1/SampleRate. При частоте дискретизации 44100 Гц это очень маленькое время — 0.00002267573 секунды.
Теперь мы можем знать, сколько времени в секундах прошло с момента старта до текущего семпла — заведем переменную _time и будем прибавлять к ней timeDelta каждую итерацию цикла.
Чтобы воспользоваться функцией WaveGenerator.GetTableSample нужно знать относительное время от 0 до 1, где 1 — период волны. Зная нужную частоты волны, мы знаем и ее период — значение, обратное частоте.
Нужное относительное время мы можем получить как дробную часть деления прошедшего времени на период волны.
Пример: мы генерируем синус со знаменитой частотой 440 Гц [48]. Из частоты находим период синуса: 1/440 = 0.00227272727 секунды.
Частота дискретизации 44100 Гц.
Рассчитаем 44150-й семпл, если на нулевом семпле время равнялось нулю.
На 44150-м семпле прошло 44150/44100 = 1.00113378685 секунд.
Смотрим, сколько это в периодах — 1.00113378685/0.00227272727 = 440.498866743.
Отбрасываем целую часть — 0.498866743. Именно это значение и нужно передать в функцию WaveGenerator.GetTableSample.
Если записать все символьно, получим:
Оформим выкладки в виде отдельной функции 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;
}
}
Обычно, в параметры осциллятора добавляют следующие:
Данные параметры есть в реализованном мною синтезаторе — вы можете самостоятельно их реализовать.
Осталось реализовать классы AudioProcessor (будет создавать осциллятор и вызывать у него метод Generate) и PluginController (создает AudioProcessor).
Посмотрите реализацию данных классов в моем коде Syntage. На текущем этапе AudioProcessor нужен, чтобы:
public class PluginCommandStub : SyntagePluginCommandStub<PluginController>
{
protected override IVstPlugin CreatePluginInstance()
{
return new PluginController();
}
}
...
public class PluginController : SyntagePlugin
{
public AudioProcessor AudioProcessor { get; }
public PluginController() : base(
"MyPlugin",
new VstProductInfo("MyPlugin", "TestCompany", 1000),
VstPluginCategory.Synth,
VstPluginCapabilities.None,
0,
new FourCharacterCode("TEST").ToInt32())
{
AudioProcessor = new AudioProcessor(this);
ParametersManager.SetParameters(AudioProcessor.CreateParameters());
ParametersManager.CreateAndSetDefaultProgram();
}
protected override IVstPluginAudioProcessor CreateAudioProcessor(IVstPluginAudioProcessor instance)
{
return AudioProcessor;
}
}
...
public class AudioProcessor : SyntageAudioProcessor
{
private readonly AudioStream _mainStream;
public readonly PluginController PluginController;
public Oscillator Oscillator { get; }
public AudioProcessor(PluginController pluginController) :
base(0, 2, 0) // у нас синт, на вход он не принимает данные, а только генерирует стерео-сигнал
{
_mainStream = (AudioStream)CreateAudioStream();
PluginController = pluginController;
Oscillator = new Oscillator(this);
}
public override IEnumerable<Parameter> CreateParameters()
{
var parameters = new List<Parameter>();
parameters.AddRange(Oscillator.CreateParameters("O"));
return parameters;
}
public override void Process(VstAudioBuffer[] inChannels, VstAudioBuffer[] outChannels)
{
base.Process(inChannels, outChannels);
// генерируем семплы
var stream = Oscillator.Generate();
// копируем полученный stream в _mainStream
_mainStream.Mix(stream, 1, _mainStream, 0);
// отправляем результат
_mainStream.WriteToVstOut(outChannels);
}
}
В следующей статье я расскажу как написать ADSR-огибающую [19].
Удачи в программировании!
P.S. В заголовке я писал что занимаюсь музыкой — если кому то интересно, можете послушать мою музыку [50], и в частности записанный diy-альбом [51].
Автор: lis355
Источник [53]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/193421
Ссылки в тексте:
[1] VST плагинов и инструментов: https://ru.wikipedia.org/wiki/Virtual_Studio_Technology
[2] VST. NET: https://vstnet.codeplex.com/
[3] моего простого синтезатора: https://github.com/lis355/Syntage
[4] Загадочный мир синтеза звука: #Part1
[5] Звук в цифровом виде: #Part2
[6] VST SDK: #Part3
[7] WDL-OL и JUCE: #Part4
[8] VST .NET: #Part5
[9] Моя надстройка над VST .NET: #Part6
[10] WPF UI: #Part7
[11] UI-поток: #Part8
[12] Обзор архитектуры синтезатора Syntage: #Part9
[13] Настраиваем проект для создания плагина/инструмента: #Part10
[14] Отладка кода: #Part11
[15] Пишем простой осциллятор: #Part12
[16] Список литературы: #Part13
[17] Летучий Корабль: https://www.youtube.com/watch?v=Zv5lIQPRCPY
[18] генератор волны (осциллятор): http://ru.wikipedia.org/wiki/Генератор_сигналов
[19] ADSR огибающая сигнала: http://ru.wikipedia.org/wiki/ADSR-огибающая
[20] фильтр частот: http://ru.wikipedia.org/wiki/Цифровой_фильтр
[21] эхо/дилей: http://ru.wikipedia.org/wiki/Дилэй
[22] статью на хабре "Теория звука": https://habrahabr.ru/company/yandex/blog/270765/
[23] эквализацию: http://ru.wikipedia.org/wiki/Эквалайзер
[24] клиппинг: http://ru.wikipedia.org/wiki/Клиппинг_(аудио)
[25] тысячи их: https://en.wikipedia.org/wiki/Audio_plug-in
[26] DAW: http://en.wikipedia.org/wiki/Digital_audio_workstation
[27] официального сайта: http://www.steinberg.net/en/company/developer.html
[28] официального сайта: http://www.steinberg.net/en/company/developers.html
[29] знаменитый эффект rise up: https://www.audioblocks.com/stock-audio/sci-fi-rise-and-and-vanish-sound-101335.html
[30] cutoff (частота среза): http://fierymusic.ru/rabota-so-zvukom/obrabotka-zvuka/chto-takoe-ekvalayzer
[31] WDL-OL: https://github.com/olilarkin/wdl-ol
[32] Martin Finke's Blog "Music & Programming": http://www.martin-finke.de/blog
[33] хабр статьи-переводы на русский: https://habrahabr.ru/post/224911/
[34] JUCE: http://www.juce.com
[35] хотя бы, на вики: http://en.wikipedia.org/wiki/JUCE
[36] vstnet.codeplex.com: http://vstnet.codeplex.com
[37] линк: https://vstnet.codeplex.com/downloads/get/910521
[38] линк: https://midinet.codeplex.com/downloads/get/664848
[39] линк: https://naudio.codeplex.com/downloads/get/1436305
[40] mda: http://mda.smartelectronix.com/
[41] осциллографа: http://ru.wikipedia.org/wiki/Осциллограф
[42] Bypass: http://ru.wikipedia.org/wiki/Байпас
[43] дисторшн: http://ru.wikipedia.org/wiki/Дисторшн
[44] Фурье сказал: https://habrahabr.ru/post/196374/
[45] тембром: http://ru.wikipedia.org/wiki/Тембр
[46] спектром: https://en.wikipedia.org/wiki/Spectrum
[47] белый шум: http://ru.wikipedia.org/wiki/Белый_шум
[48] 440 Гц: http://ru.wikipedia.org/wiki/Ля_(нота)
[49] wah-wah: http://ru.wikipedia.org/wiki/Wah-wah
[50] мою музыку: https://lartsov.bandcamp.com/album/music
[51] записанный diy-альбом: https://lartsov.bandcamp.com/album/17
[52] Модульные аналоговые синтезаторы: https://habrahabr.ru/post/236703/
[53] Источник: https://habrahabr.ru/post/311220/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.