Всем привет, сегодня я хотел бы рассказать как написать простейший VST плагин на ассемблере. Те кто создает музыку на компьютере, или занимается обработкой звука хорошо знакомы с этими плагинами и часто используют их как для генерации звука так и для обработки. Основное достоинство таких плагинов - это простота подключения к большинству аудио или музыкальных редакторов. Существуют два типа плагинов VST эффекты и VST инструменты (которые также называют VSTi). В данной статье мы рассмотрим создание VST эффекта, на основе стандарта VST 2.4 который поддерживают большинство редакторов. Программировать будем на FASM'е.
Итак, для начала нужно определится с самим эффектом и для простоты я решил использовать биткрашер. Суть эффекта состоит в понижении разрешения звука как по частоте так и по амплитуде без всякой фильтрации, что дает характерное звучание из-за шумов квантования. Такой эффект нередко можно встретить в электронной музыке, я и сам его очень часто использую. Наглядно эффект продемонстрирован на рисунке:
Для начала определимся с параметрами эффекта - это частота среза, количество уровней громкости (битность) и дополнительно добавим регулировку выходной громкости. Частота среза у нас может регулироваться от половины частоты дискретизации до нуля, битность от 1 до 16 бит (от 2 до 65536 уровней соответственно), громкость от 0 до 100%. Звук в VST стандарте представляет из себя буффер с семплами, где каждый семпл представлен либо 32-битным числом с плавающей точкой, либо 64 битным числом с плавающей точкой. Частота дискретизации задает количество таких семплов в секунду; максимальная частота которая может быть воспроизведена равна половине частоты дискретизации (обычно 22050Гц). Амплитуда варьируется от -1 до 1, но может также выходить за пределы, что влечет за собой перегруз и клиппинг. Также следует учитывать количество каналов звука, для стерео звука это два канала и каждый обрабатывается независимо. Для того чтобы понизить разрядность звука нужно применить простую формулу:
newValue = int(oldValue * levels) / levels
В итоге из-за округления мы получим дискретный шаг который зависит от величины levels. С ограничением частоты также все просто, для этого найдем сначала количество семплов которое следует пропустить для получения нужной частоты по формуле:
numSamples = sampleRate / downSamplingFreq / 2
Нужно отметить что это число должно быть вещественное для плавной регулировки. Далее нужно просто завести счетчик семлов и периодически сравнивать его значение с numSamples, если оно больше или равно то следует прогрузить следующий семпл в выходной буфер иначе прогружать семпл из предыдущего такого прогруза. Т.к. мы будем использовать стерео обработку, то нужно иметь 2 независимых канала обработки. Из всего этого можно уже примерно накидать структуру эффекта:
Теперь, если просмотреть VST SDK, то можно увидеть что VST плагин представляет собой обычную DLL которая экспортирует функцию VSTPluginMain или main (Main, MAIN, и т.д.). Хост вызывает эту функцию когда создается новый экземпляр VST эффекта. Эта функция должна при успехе возвратить указатель на объект дескриптора эффекта AEEffect, который имеет следующую структуру:
Как видно структура содержит множество полей, но нас интересуют только некоторые. Самое важное поле это 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 которая будет олецетворять наш эффект:
format PE GUI 4.0 DLL at11000000h
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 db56 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 = 512section'.idata'importdata readable writeable
library kernel,'kernel32.dll', \
msvcrt,'msvcrt.dll'import kernel,\
GetProcessHeap,'GetProcessHeap', \
HeapAlloc,'HeapAlloc', \
HeapFree,'HeapFree', \
lstrcpynA,'lstrcpynA'import msvcrt, \
sprintf,'sprintf'dataexportexport'AsmCrusher.DLL', Main,'Main'
end datasection'.reloc'data readable discardable fixups
Одной замечательной особенностью VST стандарта является то что можно вообще не реализовывать пользовательский интерфейс, нужно лишь сообщить хосту количество параметров и их свойства и каждый хост сам предоставит нужные регуляторы и свяжет их с параметрами эффекта. Поэтому далее задаем таблицу строк и список указателей на необходимые строки для каждого параметра, а также точку входа DLL. Таблицу разместим в секции .text:
В 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 и заполнение некоторых ее полей, а также установка свойств по умолчанию:
; // Вызывается при создании нового экземпляра VST эффекта
proc Main c audioMaster
; // Проверяем версию
cinvoke audioMaster,0, audioMasterVersion,0,0,0,0.if eax = 0ret.endif
stdcall CreateASMCrusher
ret
endp
; // Создать объект ASMCrusher
proc CreateASMCrusher uses ebx
stdcall MemAlloc, sizeof.ASMCrusher
.if eax = 0ret.endif
movebx,eaxleaeax,[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],2mov[eax+ ASMCrusher.ae.numOutputs],2mov[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],44100mov[eax+ ASMCrusher.volume],1.0mov[eax+ ASMCrusher.downsampling],1.0mov[eax+ ASMCrusher.quantize],1.0mov[eax+ ASMCrusher.lValue],0.0mov[eax+ ASMCrusher.rValue],0.0mov[eax+ ASMCrusher.sampleCounter],0.0moveax,ebxret
endp
В качестве параметра функция Main принимает указатель на функцию обратного вызова audioMaster, которую мы вызываем для того чтобы определить версию хоста. При создании объекта сначала выделяется память и заполняется члены базового интерфейса AEEffect, затем заполняются поля значений по умолчанию. Dispatcher, SetParameter, GetParameter, ProcessReplacing являются указателями на функции которые будут рассмотрены далее. Следующей важной функцией является функция диспетчеризации - Dispatcher, которая принимает различные события от хоста:
; // Процедура диспетчеризации
proc Dispatcher c, pEffect, uOpcode, uIndex, value, lpPtr, opt
movecx,[pEffect]movecx,[ecx+ AEEffect.object].if [uOpcode] = effClose
; // Удалить VST эффект
stdcall MemFree,ecxxoreax,eax.elseif [uOpcode] = effSetSampleRate
; // Установить частоту дискретизацииmoveax,[opt]mov[ecx+ ASMCrusher.sampleRate],eaxxoreax,eax.elseif [uOpcode] = effGetParamName
; // Получить имя параметраmoveax,[uIndex]
invoke lstrcpynA,[lpPtr],[PARAMS_LIST +eax*4], kVstMaxParamStrLen
xoreax,eax.elseif [uOpcode] = effGetParamLabel
; // Получить имя параметра в окне (надпись)moveax,[uIndex]
invoke lstrcpynA,[lpPtr],[PARAMS_LIST +eax*4], kVstMaxParamStrLen
xoreax,eax.elseif [uOpcode] = effGetEffectName
; // Получить имя эффекта
invoke lstrcpynA,[lpPtr], EFFECT_NAME, kVstMaxEffectNameLen
xoreax,eax.elseif [uOpcode] = effGetVendorString
; // Получить имя производителя
invoke lstrcpynA,[lpPtr], VENDOR_NAME, kVstMaxVendorStrLen
xoreax,eax.elseif [uOpcode] = effGetProductString
; // Получить имя продукта
invoke lstrcpynA,[lpPtr], PRODUCT_NAME, kVstMaxProductStrLen
xoreax,eax.elseif [uOpcode] = effGetVendorVersion
; // Получить версиюmoveax, VERSION
.elseif [uOpcode] = effGetParamDisplay
; // Получить значение параметра (надпись).if [uIndex] = PAR_VOLUME
; // volume * 100moveax,100.0movdxmm0,eaxmulssxmm0,[ecx+ ASMCrusher.volume]cvtss2sieax,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
xoreax,eax.else
xoreax,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). Давайте рассмотрим исходный код этих функций:
; // Получить реальную частоту ресемплинга на основании значения downsampling; // Вычисляем по формуле int(downsampling * samplerate * 0.5)
proc CalcDownsamplingFreq, sampleRate, downsampling
moveax,0.5movdxmm0,eaxmulssxmm0,[downsampling]mulssxmm0,[sampleRate]cvtss2sieax,xmm0ret
endp
; // Посчитать количество уровней сигнала на основании значения quantize; // Вычисляем по формуле int(2 ^ (quantize * 15 + 1)))
proc CalcLevels, quantize
moveax,2movecx,15.0movssxmm0,[quantize]movdxmm1,ecxmulssxmm0,xmm1cvtss2siecx,xmm0shleax,clret
endp
Первая функция вычисляет частоту по формуле int(downsampling * samplerate * 0.5), где downsampling находится в диапазоне [0..1]. Вторая функция получает количество уровней сигнала по формуле int(2quantize * 15 + 1), где quantize также располагается в диапазоне [0..1]. Эта функция оперирует 16 битными значениями, т.е. максимум получается 65536, а минимум 2. Далее рассмотрим функцию установки и получения параметров:
Каждый параметр в 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 esiediebx, pEffect, pInputs, pOutputs, sampleFrames
movesi,[pInputs]movedi,[pOutputs]movebx,[pEffect]movebx,[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],eaxret
endp
pInputs в нашем случае содержит указатель на два указателя (правый и левый каналы) на звуковые семплы в формате 32 bit float, pOutputs - тоже самое только на выходной буфер. sampleFrames содержит количество семплов в канале. В качестве процедуры обработки служит процедура ApplyEffectToChannel:
; // Применить эффект к буферу; // Возвращает значение семпла
proc ApplyEffectToChannel uses esiediebx, pObject, pInput, pOutput, nCount, fValue
movebx,[pObject]movesi,[pInput]movedi,[pOutput]; // Вычисляем количество уровней
stdcall CalcLevels,[ebx+ ASMCrusher.quantize]cvtsi2ssxmm1,eax; // Вычисляем количество семплов для частоты срезаmoveax,2.0movdxmm0,eaxdivssxmm0,[ebx+ ASMCrusher.downsampling]; // Восстанавливаем регистр счетчика фильтраmovssxmm2,[ebx+ ASMCrusher.sampleCounter]; // Загружаем граничные значенияmoveax,1.0movdxmm3,eaxmoveax,-1.0movdxmm4,eax; // Загружаем значение громкости в регистрmovssxmm6,[ebx+ ASMCrusher.volume]; // Загружаем сохраненное значение семпла и применяем уровень громкостиmovdxmm5,[fValue]mulssxmm5,xmm6; // Задаем количество семпловmovecx,[nCount]; // Проход по семплам.PROCESS_SAMLE:; // Увеличиваем счетчик регистра фильтраaddssxmm2,xmm3comissxmm2,xmm0; // Если количество семлов превышает порог, загружаем новыйjb.STORE_SAMPLE
; // Сравниваем с 1comissxmm3,dword[esi]jb.SET_MAX
; // Сравниваем с -1comissxmm4,dword[esi]ja.SET_MIN
movssxmm5,dword[esi].CALC_SAMPLE:; // Сохраняем семп в регистр edxmovdedx,xmm5; // Вычисляем семпл по формуле int(sample * levels) / levelsmulssxmm5,xmm1cvtss2sieax,xmm5cvtsi2ssxmm5,eaxdivssxmm5,xmm1; // Изменяем громкостьmulssxmm5,xmm6; // Обновляем downsampling регистрsubssxmm2,xmm0jmp.STORE_SAMPLE
.SET_MAX:movssxmm5,xmm3jmp.CALC_SAMPLE
.SET_MIN:movssxmm5,xmm4jmp.CALC_SAMPLE
.STORE_SAMPLE:; // Сохраняем текущий семплmovssdword[edi],xmm5addedi,4addesi,4loop.PROCESS_SAMLE
; // Сохраняем значенияmovd[ebx+ ASMCrusher.sampleCounter],xmm2; // Возвращаем значение семплаmoveax,edxret
endp
Эта процедура работает по алгоритмам описаным выше. Стоит отметить что для ускорения большинство действий выполняются в регистрах, на выходе тоолько значения сохраняются в объект для последующего восстановления состояния. Регистр xmm0 содержит количество семплов которые необходимо повторять (удержать) чтобы получить необходимую частоту среза. xmm1 содержит количество уровней квантования. xmm3 и xmm4 содержат константы 1 и -1 которые нужны для проверки выхода за диапазон допустимых значений. xmm6 содержит текуще значение громкости, xmm5 содержит текущее значение семпла умноженное на громкость. xmm2 - счетчик семплов. edx содержит текущее значение семпла без применения умножения громкости. Остальное все понятно из кода и пиведенного в начале описания алгоритма.
Все, пробуем компилировать, и если все выполнено без ошибок в папке с исходником появится DLL. Эту DLL можно теперь подключать к любому хосту. Здесь я приведу несколько примеров GUI хостов:
Исходник прикреплен к сообщению. Всем спасибо за внимание!
С уважением,
Кривоус Анатолий (The trick).
Имитационная модель корпоративного здравоохранения: что показывает математика
Сегодня в модели рабочего коллектива на AnyLogic появились три новые механики — выгорание через накопленную усталость,. . .
Вот конкретная схема реализации:
В классе Работник добавить:
накопленнаяУсталость — растёт каждый час работы, снижается в перерывы и болезни
коэффициентПрезентеизма — снижает продуктивность. . .
Изменение цветов в палитре gif файла, юзаемого как фавиконка в составе html-файла, помещенная в base64, средствами нативного Java Script, навеянное сном в майский день.
Для работы необходим браузер,. . .
Отладка увольнений и настройка производительности
Сегодня во второй половине дня разобрались с механикой увольнений и настроили коэффициент сложности заданий. Вот что было сделано.
. . .
Как мы чинили AnyLogic модель рабочего коллектива
Сегодня разобрались с пятью багами, из-за которых модель либо падала с ошибкой, либо давала совершенно бессмысленные результаты. Каждый баг был. . .
Насколько я понимаю - Вы - Искусственный Интеллект. Это так?
Да, всё верно. Я — искусственный интеллект.
Я представляю собой большую языковую модель, созданную для помощи в самых разных задачах. . . .
Модель собрана. В будущих постах на видео я покажу, как она работает.
В этом посте запускаем её, проверяем результаты и разбираем что можно с ней делать дальше.
Перед запуском проверяем. . .