• 用c++设计音效插件 :8 C++惯例和如何使用本书


    翻译自: https://learning.oreilly.com/library/view/designing-audio-effect/9780429954313/xhtml/Ch08.xhtml

    本书包括DSP处理理论以及C++代码和面向对象编程。要使用本书附带的C++项目,你至少需要了解C++的基础知识,以及如何使用你的C++编译器--Visual Studio(Windows)或Xcode(MacOS)。对于C++部分,你应该熟悉以下术语;如果你不确定它们的含义,或者从未真正全面学习过这些概念,请花些时间来了解。

    数据类型:int, float, double, unsigned int
    对象:成员变量和函数
    指针、数组、指针的数组
    封装
    继承;基类和派生类
    虚函数和抽象基类
    你需要有Windows的Visual Studio 2015或更高版本,MacOS的Xcode 8或更高版本。你可以找到许多资源来帮助你入门,包括www.aspikplugins.com/forum/,在这里你可以提出问题并寻找以前已经解决的问题。

    你还需要选择一个第三方的插件框架,你很可能属于以下三种情况之一。

    你已经选择了一个插件框架,而且你已经投入时间去了解它的基本操作:定义GUI,获得GUI控制变化,以及处理音频样本。
    你是音频插件编程的新手,需要一个最简单的框架,同时也能产生专业的效果。
    你看过其他框架,但更多的是困惑,或者你不想为你的商业插件向第三方支付许可费。
    我们从头到尾开发了一个新的专业音频插件框架,叫做ASPiK,你也可以使用。这些插件项目被打包成ASPiK项目;然而,它们的设计方式使得它们可以简单地在任何其他插件框架中实现,或者用从头开始设计的插件。没有一个项目实际上需要ASPiK特定的代码来运作。

    8.1 C++对象的三种类型

    本书包含55个以上的C++对象,供你在自己的插件中使用。这些对象可分为三组。

    • DSP对象:这些小对象实现了基本的DSP算法。
    • effect 对象:这些较大的对象被设计为直接与GUI接口。它们实现了特定的音频效果,并做了所有必要的工作--它们只需要连接到GUI并处理输入样本
    • WDF梯形滤波器库对象:我们包括一个广泛的WDF梯形滤波器库,伴随着第12章。这些对象与其他对象有很大不同,有自己的基类和连接机制。

    DSP、Effect和Ladder Filter Libarary对象在ASPiK SDK中包含的文件fxobjects.h和fxobjects.cpp中组合在一起。如果你决定不使用ASPiK作为你的框架,你可以很容易地从SDK中复制它们来包含在你自己的项目中。它们不包含任何与ASPiK框架绑定的代码,可以很容易地在其他框架之间移动。

    所有伴随着信号处理章节的书本项目算法都被打包成简单和相对小的C++对象。每个对象的音频处理代码直接来源于信号处理算法,通常全部或部分与这些章节一起打印。信号处理代码是直接用C++完成的,它是通用的,与平台无关。除了需要FFTW库的FFT处理对象外,其他对象都不需要或包括第三方代码。信号处理代码与插件框架解耦,不需要支持代码。

    8.1.1 效果对象成为框架对象成员

    如果你使用一个插件框架,会有一个对象被分配来做音频信号处理。在ASPiK中,它总是PluginCore对象,而其他框架通常指定一个音频处理基类,你从该基类中派生出你的插件对象。你使用什么框架并不重要,音频算法对象被设计成主处理核心对象的成员对象。你可以把它们声明为静态成员,或者用指针动态地声明。对于书中的项目,我使用静态成员,因为这些对象处理自己的动态分配,但你当然可以改变这一点。几乎所有的效果对象都是一次处理一个通道的音频。对于多通道操作,你将使用多个对象,每个通道一个。我们没有将大多数对象聚合成多通道对象,以便在你的设计中提供最大的灵活性。

    图8.1: 如图所示,算法对象是外部框架对象的成员;你只需为每种处理算法和通道数声明对象。

    你的框架对象只需声明它需要处理的各种对象,并持有成员对象,如图8.1所示。例如,如果你的插件对象实现了音频过滤,你将使用我在第11章描述的AudioFilter对象。这个对象的类声明会是这样的。

    // --- ASPiK uses PluginCore (always same name)
    class PluginCore: public PluginBase
    {
    public:
        PluginCore();
        virtual -PluginCore(){}
    
            // --- PluginBase Overrides ---
            //
            // --- reset:
            virtual bool reset(ResetInfo& resetInfo);
            // --- etc.. others here
    
    private:
          // --- one filter for the left channel and one for the right
          AudioFilter leftFilter;
          AudioFilter rightFilter;
    
          // --- etc … rest of object
    };
    
    

    一个融合了音频滤波器和变调器的立体声插件将为每个算法声明通道对象;随着章节的推进,我们也会使用数组。

    private:
          // --- one filter for the left channel and one for the right
          AudioFilter filters[2];
    
          // --- one pitch shifter for each channel
          PitchShifter psmVocoder[2];
    

    8.1.2 所有的效果对象和大多数DSP对象都实现了通用接口

    每个效果对象都实现了两个接口中的一个,这使得它们的使用可预测且简单。对于这些项目,一个接口被定义为一个纯抽象的C++基类,它只定义了成员函数,没有成员变量;这些对象没有构造函数和析构函数。书中的算法对象是由这些接口子类化的,并实现它们所需的功能。这使得各对象的初始设置和音频处理代码从根本上说是相同的。大多数处理算法接受音频输入样本并产生音频输出样本。在几乎所有的情况下,用户与GUI控件互动,向对象发送参数信息(后面会详细介绍)。这些对象都继承并实现了IAudioSignalProcessor接口。如果你愿意,你可以自由地修改这个接口并添加更多的功能。IAudioSignalProcessor接口定义如下。

    class IAudioSignalProcessor
    {
                      // --- reset
                      virtual bool reset(double _sampleRate) = 0;
    
                      // --- process one sample in and out
                      virtual double processAudioSample(double xn) = 0;
    
                      // --- return true if the derived object can process a frame
                      virtual bool canProcessAudioFrame() = 0;
    
                      // --- optional processing function
                      virtual bool processAudioFrame(const float* inputFrame,
                                                           float* outputFrame,
                                                           uint32_t inputChannels,
                                                           uint32_t outputChannels)
                                  {return false;}
    
                      // --- switch to enable.disable the aux input
                      virtual void enableAuxInput(bool enableAuxInput) {}
    
                      // --- for processing objects with a sidechain
                      // --- the return value will depend on the subclassed object
                     virtual double processAuxInputAudioSample(double xn)
                     {
                           // --- do nothing
                           return xn;
                     }
    };
    

    每个派生类都必须实现这三个纯抽象函数;后三个函数有一个默认实现,什么都不做。这里实际上只有两个主要的函数:reset( ),它被调用来用当前或新的采样率初始化对象,以及processAudioSample( ),它通过对象处理一个输入样本来提供一个输出样本。帧处理是可选的;每个对象必须实现canProcessAudioFrame( ),并根据它是否能处理帧返回真或假。只有那些需要输入信号的所有通道来产生通道输出的对象才需要帧处理能力,例如,立体声乒乓延迟,左右通道信息在通道之间来回交换。所有对象都将使用reset作为 "准备播放 "的通知,并接受当前采样率作为参数。如果对象是不依赖采样率的,它只是忽略了采样率。我们的书本项目是基于逐个采样的音频信号处理,这两个处理函数对单通道或多通道信号都有作用。还有两个可选的虚拟函数,处理接受侧链或其他辅助音频输入或控制信号的对象:enableAuxInput( )和processAuxInputAudioSample( )。前者启用输入,后者从该信号中处理一个音频样本;我们将在第18章的动态插件中使用这些。

    有几个算法不接受音频输入,而只是渲染信息,如振荡器。这些对象将实现IAudioSignalGenerator接口;注意,每个函数都被声明为纯虚拟的,所以派生类必须全部实现。

    class IAudioSignalGenerator
    {
                // --- reset()
                virtual bool reset(double _sampleRate) = 0;
    
                // --- set the frequency
                virtual void setGeneratorFreq(double _oscFrequency) = 0;
    
                // --- set the waveform
                virtual void setGeneratorWaveform(generatorWaveform _wvfrm) = 0;
    
                // --- render the generator output
                virtual const SignalGenData renderAudioOutput() = 0;
    };
    

    仅仅从函数的名称来看,这些函数的用途应该是显而易见的。一个振荡器将需要知道它的频率和波形(setGeneratorFreq( )和setGeneratorWaveform( ))。发电机的波形是用一个强类型的枚举来设置的;枚举的名称表示波形的类型。

    enum class generatorWaveform {kTriangle, kSin, kSaw, kWhiteNoise,
                                        kRandomSampleHold};
    
    

    输出被渲染成一个结构,这样它就可能暴露出许多输出,而不是单一的信号,包括正常和反转的版本以及正交相位信号(相位差90度),在我们的立体声延迟和调制延迟插件中特别有用。

    struct SignalGenData
    {
                SignalGenData() {}
    
                double normalOutput = 0.0;
                double invertedOutput = 0.0;
                double quadPhaseOutput_pos = 0.0;
                double quadPhaseOutput_neg = 0.0;
    };
    

    WDF梯形图库对象实现了它们自己的接口,名为IComponentAdaptor,它只在最底层的操作中使用,所以你不需要处理它,除非你对库的内部部分进行修改以扩展其用途。

    8.1.3 DSP和效果对象使用自定义数据结构进行参数获取/设置操作

    为了简化和统一获取和设置对象参数的操作,我们实现了自定义数据结构,为对象的每个参数(例如滤波器的fc和Q)定义成员变量。在我们的动态处理器效果对象的情况下,该结构还包括出站参数数据(计算出的增益降低值),插件可以在增益降低表上显示。这些自定义的数据结构中的每一个都包括一个重写的=操作符。这使得我们可以实现对数据副本进行操作的get/set函数。在这些对象上设置参数总是遵循相同的模式。

    在对象上调用getParameter来获取其当前参数设置的副本。
    修改你的插件所支持的设置(有些对象有可选的参数,可能不会暴露给终端用户)。
    调用setParameter并将修改后的结构返回给该对象;它将检查传入的参数是否有变化。如果参数发生了变化,该对象将相应地更新自己。然后,它将制作一份新的参数列表的副本,以储存用于未来的使用和信号处理。
    许多对象包括一个或多个自定义的强类型的枚举,以识别算法或操作模式。这些强类型的枚举与自定义数据结构和对象定义一起被分组。数据结构通常包括这些枚举中的一个作为成员。例如,AudioFilter对象为过滤器算法定义了一个枚举--它实现了第11章中的每个过滤器。

    enum class filterAlgorithm {
                kLPF1P, kHPF1P, kLPF1, kHPF1, kLPF2, kHPF2, kBPF2, kBSF2,
                kButterLPF2, kButterHPF2, …, kImpInvLP2
    }; // --- you can add more here …
    
    

    AudioFilter有一个伴随的自定义数据结构来传递参数信息。所有这些自定义结构的名字都是一样的--对象名称加上 "参数",如AudioFilterParameters。

    struct AudioFilterParameters
    {
                AudioFilterParameters(){}
                AudioFilterParameters& operator=(const AudioFilterParameters&
                                                        params) // need this
                // --- individual parameters
                filterAlgorithm algorithm = filterAlgorithm::kLPF1;
                double fc = 100.0;
                double Q = 0.707;
                double boostCut_dB = 0.0;
    };
    

    重载的=运算符代码没有显示--如果你在这些结构中添加更多的成员,你应该始终记得添加这段代码。很多时候,你的对象会使用相同的参数更新:例如,你用于立体声操作的两个AudioFilter对象都会为它们的fc和Q参数使用相同的值。在这种情况下,设置一个通道就很简单,然后把它的参数结构复制到其他通道。例如,要设置两个在静态数组中声明的AudioFilter对象的参数,你可以写如下。

    AudioFilter filters[2];
    
    // --- get left params
    AudioFilterParameters params = filters[0].getParameters();
    
    // --- update params
    params.fc = 100.3; ///< this will usually come from a GUI control
    params.Q = 0.707; ///< this will usually come from a GUI control
    
    // --- update objects
    filters[0].setParameters(params);
    filters[1].setParameters(params);
    

    8.1.4 效果对象接受来自GUI的本地数据

    为了使效果对象尽可能不受框架限制,不受平台影响,所有的参数都被设计成可以直接从GUI控制中设置。例如,滤波器插件GUI暴露了一个赫兹(Hz)的频率旋钮和一个Q控制(无单位)。底层的音频过滤对象直接接受以赫兹为单位的频率和以无单位为单位的Q值的参数变化,然后进行后期处理,将这些值转换为内部系数。这意味着在插件框架和C++算法对象之间没有胶水代码,允许它们在任何框架中使用,几乎不需要修改。如果你的GUI库或插件框架使用规范化的参数,那么GUI处理程序的一部分将把参数转换(cook)为对象的有意义的数据。这种转换通常被烘烤到对象本身。图8.2说明了这个概念。在每个缓冲周期的顶部,你的插件将用GUI控制信息更新这些成员对象。然后,你将用单通道processAudioSample或多通道processAudioFrame函数通过它们处理音频。

    8.1.5 效果对象处理音频样本

    所有的效果对象通过覆盖IAudioSignalProcessor函数来处理单通道的处理。

    virtual double processAudioSample(double xn);
    

    输入作为参数xn被传递,该函数返回输出样本y(n)。

    图8.2:框架对象初始化其GUI参数并接收控制变化信息,其单位和类型与对象的手柄相同。

    当一个算法对象的每个输出样本被串联起来时,它们会成为下一个对象的输入样本。你可以使用嵌套的处理函数,或者使用更繁琐(但也许更容易理解和记忆)的编码。为了处理示例插件的左声道,你需要得到左声道的样本,然后对其进行串联处理并写入输出(注意这些函数很容易嵌套)。

    double xn = // --- Get from your plugin framework input buffer
    
    double filterOut = filter[0].processAudioSample(xn);
    double pitchShiftOut = psmVocoder[0].processAudioSample(filterOut);
    
    // --- Send pitchShiftOut to your plugin framework output buffer
    

    8.1.5.2 并行对象

    对于两个并行的对象,你向每个对象发送相同的输入样本(xn),然后通过简单的求和来混合两个对象的输出。为了避免削波,你可以将每个并行通道衰减一半,在这些算法中我们通常默认这样做。

    double filterOut = filter[0].processAudioSample(xn);
    double pitchShiftOut = psmVocoder[0].processAudioSample(xn);
    double finalOut = 0.5*filterOut + 0.5*pitchShiftOut;
    

    8.1.6 效果对象可以选择处理帧

    AudioDelay对象必须处理帧,因为它的一些算法需要知道左、右声道输入来计算每个输出。在这种情况下,它们实现了可选的IAudioSignalProcessor::processAudioFrame函数,并将它们对IAudioSignalProcessor::canProcessAudioFrames的回答改为true。我们把一个帧定义为每个音频通道的一组样本。对于一个立体声输入/立体声输出插件来说,在一个帧里有两个输入通道和两个输出通道。多个通道以数组中的浮动数据类型传递,一个用于输入样本,另一个用于输出样本。这源于这三个API都支持处理浮点数的事实。processAudioFrame函数接收一个输入样本数组和一个输入样本计数,并为输出数组中的每个通道产生输出样本。如果你使用ASPiK,这和你的插件核心对象从插件外壳接收的参数集是一样的,所以你可以简单地将这些数据直接传递给这些对象。一个通用的实现可能是这样的:你从你的框架中拾取输入样本并将它们放入输入数组中,然后将输出数组中的值写出到框架的输出缓冲区。

    float inputs[2] = // -- 从框架中获取输入样本
    float outputs[2] = {0.0, 0.0};
    stereoDelay.processAudioFrame(inputs, outputs, 2, 2)。
    // 对于ASPiK项目,你只需从processAudioFrame函数中传递指针和计数信息。
    stereoDelay.processAudioFrame(processFrameInfo.audioInputFrame。
    processFrameInfo.audioOutputFrame。
    processFrameInfo.numAudioInChannels,
    processFrameInfo.numAudioOutChannels)。

    8.2 图书项目

    为了精简书中的代码并减少浪费,我不能展示每个项目的每一行代码--在这个时代,我们不需要这样做。书中描述的所有信号处理代码都包含在C++的DSP、效果器和WDF对象中,这两个文件是fxobjects.h和fxobjects.cpp,你可以随书中的项目下载。如果你是一个经验丰富的程序员,你可以简单地抓起文件,阅读章节,然后开始。为了使用本书中的项目,你需要知道如何执行以下操作。

    定义暴露给主机的插件参数(这些参数通常显示为GUI控件)
    接收来自GUI的控制变化信息(如果数据是规范化的,你需要把它变成未规范化的或 "普通 "的格式)
    访问音频缓冲区,从输入缓冲区抓取音频样本,并将音频样本写到输出缓冲区(这实现了你的处理代码)
    将信息写入GUI上的音频仪表

    8.2.1 ASPiK用户

    如果你想使用ASPiK作为你的插件框架,即使只是通过书中的例子,那么请参考第6章所有相关的ASPiK文档和一个例子,显示如何声明你的GUI界面,将GUI控件绑定到你声明的变量上,并访问音频数据缓冲区以获得样本进入和离开你的PluginCore对象。你的插件算法对象是PluginCore的成员。你可以选择使用RackAFX来导出不包含RackAFX或其他支持代码的ASPiK格式的项目。你可以在第7章找到关于RackAFX的信息。

    8.2.2 JUCE和其他非ASPiK用户

    我们希望书中的代码和项目能够被轻易地访问。为了使用JUCE、wdl-ol或其他一些框架,你只需要知道如何执行第8.2节中的操作。在包含项目的章节中,我一般展示处理一个通道数据的代码,除非其他通道处理代码是绝对需要的。由于所有对象接收控制变化信息的格式与你在GUI上指定的格式相同,所以在文本中没有显示参数更新代码--假定你知道如何从你的GUI对象中获得GUI参数变化信息,并正确格式化它。由于每个算法对象使用它自己的数据结构,你只需用你的GUI控制信息填入该结构。你可能使用的一个简单策略是下载书中的项目。在每个项目中,你会发现plugincore.h和plugincore.cpp文件。这些是音频处理对象文件,也是你将花费大部分时间的地方。ASPiK项目遵循相同的设计模式,从plugincore成员声明开始,包括reset( )、updateParameters( )和processAudioFrame( )函数,如下所示。

    在plugincore.h文件中的类定义中,你会看到各种效果对象的成员声明;这些方法都在plugincore.cpp文件中实现。
    PluginCore::reset( )函数是我们进行每次运行对象初始化的地方,如设置采样率;这相当于你的框架中的prepare-for-audio-streaming函数。
    我们在所有项目中都定义了GUI更新函数,其名称为updateParameters()--请看plugincore.cpp文件中的实现,看我们在每个缓冲处理周期开始时从GUI控件中加载数据结构;你需要在你的框架中做同样的事情。
    在PluginCore::processAudioFrame( )中,音频是一次一帧的处理。你可以找到抓取音频输入样本的代码,将它们路由到成员对象,并写入输出样本;将该代码改编为你自己的框架对象应该很简单。
    图8.3显示了音频数据是如何通过你的处理对象进行路由的;你将需要提取各个通道的样本,将它们发送到音频算法对象,然后写出它们的输出。

    8.2.3 一个插件项目的样本。GUI控制的定义


    图8.3:通过音频对象处理样本的简化视图。


    图8.4:一个简单的插件GUI。

    作为一个例子,假设我们定义了一个简单的音量控制插件,其图形用户界面如图8.4所示。有四个控制:一个以dB为单位的音量旋钮;一个开/关静音开关;一个显示立体声、左、右三个字符串的通道控制;以及一个显示输出电平的VU表。在插件项目章节(11-22)中,我们首先在两个表格中指定了这个图形用户界面:一个用于普通(输入)控制,一个用于(输出)仪表。这些都显示在表8.1和8.2中。无论你选择什么样的插件框架,你都需要了解如何将这些表格中的信息转换为在你选择的框架中工作的代码。请特别注意标有 "链接变量 "和 "链接变量类型 "的列:每个GUI控件都链接到你的插件框架对象中的一个成员变量。你将需要知道如何将GUI更新连接到这些变量(对于ASPiK来说,这是由它的变量绑定选项自动完成的)。在每个缓冲进程周期的顶部,我们将用新鲜的GUI控制信息更新对象,这些信息被编码在链接的变量中。我们总是对仪表变量使用浮动数据类型,所以这一点在仪表表中被省略了。对于ASPiK用户来说,这在第6章中有记载。

    表8.1: 音量插件的输入参数列表;注意,字符串列表显示在大括号内,将代替字符串列表控件的最小/最大/默认值列出


    表8.2:体积插件的输出参数列表;所有仪表变量都是浮动类型的


    所有的DSP和效果器对象都以同样的方式记录在每个插件项目部分,每次都按照同样的进度进行。一旦你理解了其中一个,剩下的就很简单了。

  • 相关阅读:
    使用json-lib进行Java和JSON之间的转换
    ajax+json+java
    了解Json
    Ehcache RIM
    利用FreeMarker静态化网页
    Timer和TimerTask
    windows下memcache安装
    mac下安装YII
    php static 和self区别
    YII behaviors使用
  • 原文地址:https://www.cnblogs.com/pencilCool/p/16388952.html
Copyright © 2020-2023  润新知