DEATHTOUCH 发表于 2021-6-14 11:20

【Pascal】VST2.4 效果器插件的简单实现 (FPC&Delphi)

本帖最后由 DEATHTOUCH 于 2023-3-20 13:08 编辑

注意
本文内容可能随时间会存在与实际仓库内容不符的问题

前言VST 对音乐制作人来说是非常熟悉的,它可以做效果器,也可以做音源(VSTi),由于本人对VST插件技术比较感兴趣,所以就专门研究了下,但是网上相关资料极少,遇到了不少问题。

简单介绍下VSTVST全称 Virtual Studio Technology,译为虚拟工作室技术,由 Steinberg 公司开发。VST1.0 于1996年发布;VST2.0 在1999年发布,并于2006年更新到了2.4版本,也是非常流行的一个版本,尽管 Steinberg 在2013年宣布停止对 VST2 的维护;VST3.0 于2008年发布,截至2021年9月已经更新到了3.7.3版本。
或者再简单一点,它就是一套 C/C++ 代码,到了3.0以后就几乎是C++了。

使用VST2.4的原因尽管停止了支持,但是 VST2.4 还是非常流行,而且 VST2.4 已经得到了时间的检验,稳定易用,非常适合入门,而网上基本没有用 Pascal 对 VST2.4 的简单实现(DelphiASIOVST 封装了好多,不适合对基本原理的学习),所以我选择使用 VST2.4 并用 Pascal 语言来实现它。当然还有一个重要原因是VST3与C++深度绑定,官方甚至写了不少工具和库,使得转换到其他语言的难度非常大。
由于本人水平不高,搞得不好,可能有不少毛病,请各位见谅。

基本使用注意:由于涉及到了 VST 的基本原理,以下内容可能存在一定难度。

我们只使用 vst2intf.pas 基本接口文件,这个文件是基于官方的aeffect.h和aeffectx.h的,由于没有任何封装,所以我们仅仅实现最基础的功能。一个快捷的方式是使用vst2wrapper.pas单元提供的基本封装类TVst2Wrapper,这个类是按照官方AudioEffect和AudioEffectX这两个类进行了一定的修改和扩充而来的。

首先新建工程,选择动态链接库,我们把它保存为 hello.lpr(Delphi是hello.dpr),然后我们暂时不管它。
下一步新建单元,我们命名为 main.pas 并写入如下代码
unit main;

interface

uses
vst2intf;

type
TMyPlugin = class
private
    FHost:THostCallback;
    FEffect:TAEffect;
public
    constructor Create(AHost:THostCallback);
    destructor Destroy;override;
    function GetEffect:PAEffect;
    function dispatcher(opcode:TAEffectOpcodes;index:Int32;value:IntPtr;ptr:Pointer;opt:Single):IntPtr;
    procedure process32(inputs,outputs:TBuffer32;nframes:Integer);
end;

implementation

function DispatcherCallback(e:PAEffect;opcode,index:Int32;value:IntPtr;ptr:Pointer;opt:Single):IntPtr;cdecl;
var
v:TMyPlugin;
begin
v:=TMyPlugin(e^.Obj);
if (opcode>=0) and (opcode<kVstAEOpcodeMax) then
    if opcode<>ord(effClose) then
      Result:=v.dispatcher(TAEffectOpcodes(opcode),index,value,ptr,opt)
    else begin
      v.dispatcher(TAEffectOpcodes(effClose),0,0,nil,0);
      v.Free;
      Result:=1;
    end
else
    Result:=0;
end;

procedure Process32Callback(e:PAEffect;inputs,outputs:TBuffer32;nframes:Integer);cdecl;
begin
TMyPlugin(e^.Obj).process32(inputs,outputs,nframes);
end;

constructor TMyPlugin.Create(AHost:THostCallback);
begin
FHost:=AHost;
FEffect.Magic:=kEffectMagic;
FEffect.UniqueID:=MakeLong('HeLo');
FEffect.Obj:=self;
FEffect.Dispatcher:=@DispatcherCallback;
FEffect.Process:=@Process32Callback;
FEffect.NumInputs:=2;
FEffect.NumOutputs:=2;
end;

destructor TMyPlugin.Destroy;
begin
inherited Destroy;
end;

function TMyPlugin.GetEffect:PAEffect;
begin
Result:=@FEffect;
end;

function TMyPlugin.dispatcher(opcode:TAEffectOpcodes;index:Int32;value:IntPtr;ptr:Pointer;opt:Single):IntPtr;
begin
Result:=0;
case opcode of
    effGetVstVersion:Result:=kVstVersion;
    else;
end;
end;

{$PointerMath On}

procedure TMyPlugin.process32(inputs,outputs:TBuffer32;nframes:Integer);
var
i:Integer;
begin
for i:=0 to nframes-1 do
begin
    outputs:=inputs*0.5;
    outputs:=inputs*0.5;
end;
end;

end.

然后在 hello.lpr 写入如下代码
library hello;

uses
vst2intf, main;

function VSTPluginMain(Host:THostCallback):PAEffect;cdecl;
begin
    Result:=TMyPlugin.Create(Host).GetEffect;
end;

exports
VSTPluginMain;

begin
end.

然后编译即可获得 hello.dll,一个没有 GUI 的最简单的 VST2.4 插件,功能是把输入的音频信号振幅减半。

下面简单解释一下部分代码,不过在了解代码之前先介绍下 TAEffect 结构type
TAEffect = record
    Magic:         Int32;// must be kEffectMagic,也就是常量'VstP'
    Dispatcher:    TAEDispatcherCb; // Host to Plug-in dispatcher,由插件实现,提供给宿主软件的
    Process:       TAEProcess32Cb; // deprecated, default process callback function before 2.4,处理音频,已废弃
    SetParameter:TAESetParamCb; // Set new value of automatable parameter,设置参数
    GetParameter:TAEGetParamCb; // Returns current value of automatable parameter,读取参数
    NumPrograms:   Int32;// number of programs, or we can say presets,插件预制(Preset)的数量
    NumParams:   Int32;// number of parameters, all parameter are included in preset,插件参数的数量
    NumInputs:   Int32;// number of audio inputs,输入通道数
    NumOutputs:    Int32;// number of audio outputs,输出通道数
    Flags:         TVstAEffectFlags; // See TVstAEffectFlags,一些插件的功能标志位
    Resvd1:      IntPtr; // reserved for Host, must be 0,保留给宿主
    Resvd2:      IntPtr; // reserved for Host, must be 0,保留给宿主
    { For algorithms which need input in the first place(Group delay or latency in Samples).
      This value should be initialized in a resume state. }
    InitialDelay:Int32;
    RealQualities: Int32;   // deprecated unused member
    OffQualities:Int32;   // deprecated unused member
    IORatio:       single;// deprecated unused member
    Obj:         Pointer; // The plugin class pointer,插件类对象,方便插件自己调用
    User:          Pointer; // user-defined pointer,给插件开发者提供的额外指针,方便开发
    { Registered unique identifier(register it at Steinberg 3rd party support Web).
      This is used to identify a plug-in during save+load of preset and project. }
    UniqueID:      Int32; // 必须独一无二,当然现在Steinberg官网也不接受新的申请了
    Version:       Int32; // plug-in version (example 1100 for version 1.1.0.0),插件版本,开发者自己决定
    ProcessReplacing: TAEProcess32Cb; // Process audio samples in replacing mode, default in 2.4,vst2.4默认的处理函数
{$ifdef VST_2_4_EXTENSIONS}
    // Process double-precision audio samples in replacing mode, optional in 2.4
    ProcessDoubleReplacing: TAEProcess64Cb; // vst2.4可选的处理函数,支持64位浮点数,精度更高
    Future: array of byte; // reserved for future use (please zero)
{$else}
    Future: array of byte; // reserved for future use (please zero)
{$endif}
end;
然后我们看一下构造函数的内容
constructor TMyPlugin.Create(AHost:THostCallback);
begin
FHost:=AHost;
FEffect.Magic:=kEffectMagic;
FEffect.UniqueID:=MakeLong('HeLo');
FEffect.Obj:=self;
FEffect.Dispatcher:=@DispatcherCallback;
FEffect.Process:=@Process32Callback;
FEffect.NumInputs:=2;
FEffect.NumOutputs:=2;
end;
注意到我们仅用了部分字段,下面一个个介绍一下

[*]FHost 是入口函数传入的参数,用于让插件主动调用主机的功能
[*]Magic 是必须设置的,且一定为常量 kEffectMagic,代表“VstP”
[*]UniqueID 也是必须设置的,使用 MakeLong 函数可以用 4 个字符组成的字符串进行生成,字符串由自己确定,尽可能做到独一无二
[*]Obj 是类自身的对象指针,用来调用相关回调函数使用
[*]Dispatcher 是 TAEDispatcherCb 类型的回调函数指针,用于主机对插件的操作
[*]Process 是 TAEProcess32Cb 类型的回调函数指针,用于主机对插件的 DSP 部分调用
[*]NumInputs 和 NumOutputs 是输入输出的通道数,都设置成 2 说明立体声输入输出,这个会影响到Process函数的通道数量

注意:这里的回调函数都是用 cdecl 的调用方式

现在我们看看回调函数 Dispatcher
function DispatcherCallback(e:PAEffect;opcode,index:Int32;value:IntPtr;ptr:Pointer;opt:Single):IntPtr;cdecl;
var
v:TMyPlugin;
begin
v:=TMyPlugin(e^.Obj);
if (opcode>=0) and (opcode<kVstAEOpcodeMax) then
    if opcode<>ord(effClose) then
      Result:=v.dispatcher(TAEffectOpcodes(opcode),index,value,ptr,opt)
    else begin
      v.dispatcher(TAEffectOpcodes(effClose),0,0,nil,0);
      v.Free;
      Result:=1;
    end
else
    Result:=0;
end;
可以看到当主机发出 effClose 指令的时候就需要由插件手动释放 Obj 指向的对象,实现插件的卸载,否则让插件响应主机的指令,具体可以看 TAEffectOpcodes 的所有指令

我们顺便看看 dispatcher 函数
function TMyPlugin.dispatcher(opcode:TAEffectOpcodes;index:Int32;value:IntPtr;ptr:Pointer;opt:Single):IntPtr;
begin
Result:=0;
case opcode of
    effGetVstVersion:Result:=kVstVersion;
    else;
end;
end;
我们只响应 effGetVstVersion,返回 VST 版本,kVstVersion 是一个常量,2400表示版本是 2.4,注意这里一定要设置,否则宿主会认不出来
如果要响应其他内容可以在这里完善,比如响应 effProcessEvents 就可以对 MIDI 进行处理,不过想要实现要相对再复杂些

再看看回调函数 Process32Callback
procedure Process32Callback(e:PAEffect;inputs,outputs:TBuffer32;nframes:Integer);cdecl;
begin
TMyPlugin(e^.Obj).process32(inputs,outputs,nframes);
end;
我们就直接调用 Obj 的 process32 方法就行,然后我们看看 process32
{$PointerMath On}
procedure TMyPlugin.process32(inputs,outputs:TBuffer32;nframes:Integer);
var
i:Integer;
begin
for i:=0 to nframes-1 do
begin
    outputs:=inputs*0.5;
    outputs:=inputs*0.5;
end;
end;
注意这个编译器指令 {$PointerMath On},打开这个之后下面的内容才能编译通过,否则会报错,该功能至少需要 Delphi 2009 版本
该函数的作用就是根据所给的缓冲区长度 nframes,处理 inputs 的数据并填充 outputs 的数据
比如这里就是简单的把输入的左右声道的振幅减半再输出,注意振幅减半实际相当于使分贝-6
补充:inputs 和 outputs 都是二级指针,以 inputs 为例,inputs指左声道,inputs指右声道,具体通道数取决于插件自行设置的数量,每个声道指向一片连续的单精度浮点数数组,且每一个数都是量化到 [-1.0 ~ 1.0],如果超出范围播放就会出现失真,具体可以了解一下 PCM 编码中的 32bit float 编码。


最后再看看导出的函数 VSTPluginMain,注意因为这个函数名需要导出,所以区分大小写,而且一定要是 cdecl 的调用约定
function VSTPluginMain(Host:THostCallback):PAEffect;cdecl;
begin
    Result:=TMyPlugin.Create(Host).GetEffect;
end;
本质就是创建了我们的对象并返回 TAEffect 结构的指针供主机使用

然后导出这个函数就行了
exports
VSTPluginMain;

其实仔细看就会发现,VST2.4 的通信机制就是使用回调函数,作为插件方只需要实现 dispatcher 函数所需的大部分功能即可。
比如要使用 GUI 可以使用各种界面库,然后完成 effEditGetRect,effEditOpen,effEditClose 这几个指令就可以了。
使用 Pascal 最大的好处就是可以快速完成界面库的开发(基于 VCL 库或者 LCL 库),而且 Pascal 语言的严谨性有助于 DSP 代码的编写。

源码下载因为代码量挺大,好几千行,我提供仓库链接、网盘链接和附件。(会不定时更新例子或者修bug)
具体一些细节看 README 文件
vst24pas: 使用 Pascal 实现的 VST 2.4 (gitee.com)
vst24pas: The pascal implementation of VST 2.4 (github.com)
https://lanzoui.com/b010jq7cb 密码 ht6g
https://lanzoux.com/b010jq7cb 密码 ht6g
可以从这些地方下载到源码。

结语
以上只是一个简单的实现,具体请看 examples 下的例子,许多问题也可以多看看源码,主要是 vst2intf.pas 文件,其他文件都是基于此的一些封装。
由于时间和水平有限,目前只有几个简单的例子,可以了解参数和GUI的使用。
如果要做VSTi音源,就需要更多对于 MIDI 的知识;至于要更高级的效果,那又是关于DSP的知识了。

还有好多需要学习的......

关于 VST3

目前简单研究了下 VST3 的内容,发现 VST3 使用类似于 COM 组件的模式,大量使用接口,而由于 C++ 和 Pascal 的接口存在一定的差别,所以可能无法实现完美的兼容性,对于 VST3 的 Pascal 化存在较大的难度。

而且Delphi的接口只支持COM模式的,编译器会有自动的代码,不利于操作,尽管Delphi 2010之后有和等一系列标注,但实际上还是有点麻烦。

tek2y 发表于 2021-6-14 12:27

学习了,感谢分享经验

宜城小站 发表于 2021-6-14 13:18

谢谢楼主分享{:1_893:}
20多年没有编程了{:1_925:}
看了好亲切:handshake

zxxiaopi 发表于 2021-6-14 13:43

在用delphi,感谢分享

第一品霄 发表于 2021-6-14 14:08

学习一下了

lies2014 发表于 2021-6-14 15:11

谢谢分享,有空研究一下VST3

hansxia 发表于 2021-6-14 15:34


在用 10.4.2,貌似我有个钢琴模拟的软件是用到了这个代码,不过我注释掉了


tk123wc 发表于 2023-3-20 00:49

进我的收藏夹里吃灰吧

冥界3大法王 发表于 2023-5-14 10:42

@DEATHTOUCH 楼主是我见到论坛仅存的Delphi高手

DEATHTOUCH 发表于 2023-5-14 12:08

冥界3大法王 发表于 2023-5-14 10:42
@DEATHTOUCH 楼主是我见到论坛仅存的Delphi高手

相信论坛一定有更厉害的大佬,可能只是不怎么来。
这个VST2插件都是两年前搞的了,VST3搞了一半,目前搁置了。
页: [1]
查看完整版本: 【Pascal】VST2.4 效果器插件的简单实现 (FPC&Delphi)