我的笔记

灵感、随想与新技术
06 6月 2020

音频开发进阶(二)使用API文档 (附源码·混响效果器)

 

框架的时代

 

如今,我们输出一行 “Hello World” 到屏幕上,已经不需要手动地把数据移动到显存里面了。一句简单地语句就可以搞定。高级程序语言本身就是一种框架。

不要重复发明轮子

当你要开发的功能 已经存在了成熟的框架,我劝你最好使用它们。

相比一个人从头开始写底层功能,现成的框架已经有了一段时间的积累,它们更稳定,更安全,优化更好,而且还方便。

当然如果你对某些功能有更深入的理解,或是为了长远的打算,编写一个框架也是件很了不起的事。

 

无论用什么框架开发,都离不开对开发文档的查阅

 

本节我们就了解一下 如何使用 JUCE 开发文档,编写一个 Reverb 混响插件。

 

1.明确需求

一个混响效果器

2.收集信息

 

我们先来到 JUCE的开发文档(点击打开), 看看有没有用得上的东西。

网页里,按下Ctrl + F (Mac 上 Command + F), 搜索 reverb 然后回车

找到这个,应该是我们想要的

点进去,看到 Description 里说了,先用 setSampleRate 接口准备一下,然后用 processStereo 或者 processMono 处理信号就行了

看完说明,就大概了解了它的用法。(以后我们接触的东西多了,就不用看说明了,这些方法大同小异)

然而现在我们还有一个需求没有得到解决,那就是调节参数。

继续寻找,在上方的 Member Functions 功能接口的描述中,看到了 setParameters 接口,它的传入参数是一个 ‘Parameters’ 类型 的东西,我们点一下那个 Parameters。

发现它是个结构体,里面的字段就是混响的参数,这些参数都是默认初始化过的。

好了,现在已经收集到需要的全部信息了。

 

3.整理思路

 

我们看到上面 Reverb 的文档中写的都是些成员方法,理所当然就想到实例化一个 Reverb 的对象来用,在JUCE里,这是不必要的。
Reverb 是来自JUCE库里的类,它的对象不用 new 不用 Reverb() 实例化创建,只要声明了它,JUCE 框架就会自动为我们注入一个合适的 Reverb 对象。所以把它当成结构体就可以了,【Reverb 对象声明完直接使用】。(这是一个好架构,它让开发者专注于业务逻辑,避免分心

有别于其他语言,C++实例化对象是可以不new的。

一般来说对象都是保存在堆上,但C++可以通过这种不new的方式直接把它保存在栈上,这样实例化出来的对象只作用于函数内部,函数执行后就删除了。

 

setParameters 用到的参数是个结构体,值类型的声明后直接用就行了。

Reverb 对象需要在使用前调用一下 【setSampleRate】接口。在《音频开发实战(二)》的时候,我们使用了一个 getSampleRate 接口获取采样频率,而这个接口并不是每一次都有返回。为了更好的稳定性,我们选用PluginProcessor 的回调 【prepareToPlay (double sampleRate, int samplesPerBlock) 】中的采样频率。

最后在 processBlock 中用 【processStereo】 接口处理立体声,这个接口的传入的参数是 左边的信号序列指针,右边的信号序列指针,处理多少个点。
这三个参数刚好就是 buffer.getWritePointer(0), buffer.getWritePointer(1), buffer.getNumSamples()。

好了,我们已经知道怎么做了

 

4.付诸行动

 

-> 数据处理

首先在 processor 的头文件准备好 Reverb 对象 和 它的 参数结构体

1
2
3
4
5
6
// PluginProcessor.h
...
Reverb reverbInstance;
Reverb::Parameters reverbParameters;
private:
...

然后在 prepareToPlay 回调里,调用 Reverb 对象的 setSampleRate 接口

1
2
3
4
5
6
7
// PluginProcessor.cpp
...
void MyReverbAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
reverbInstance.setSampleRate(sampleRate);
}
...

在信号处理的回调 processBlock 中, 把 buffer 写入指针 和 采样点数量的信息 直接传给 Reverb 对象的 processStereo 接口。

1
2
3
4
5
6
7
8
9
10
11
12
// PluginProcessor.cpp
...
void MyReverbAudioProcessor::processBlock (AudioBuffer& buffer, MidiBuffer& midiMessages)
{
ScopedNoDenormals noDenormals;

for (auto sample = 0; sample < buffer.getNumSamples(); sample++)
{
reverbInstance.processStereo(buffer.getWritePointer(0), buffer.getWritePointer(1), buffer.getNumSamples());
}
}
...

运算的部分就写完了。

那么 setParameters 在哪呢? 我们还没有写,要知道processor的初始化是早于editor的,毕竟它是作为editor构造方法的参数被传入的,那既然 processor 中没有调用 setParameters 接口,在设置参数前没有参数的情况下 processStereo 被调用了怎么办呢。

没有关系,Reverb 已经为我们初始化了一个默认的结构体,这个默认的结构体里包含默认的参数值。

F12 到 Reverb 里看一下它的定义

 

-> 界面

在头文件里声明一个推子

1
2
3
4
5
6
// PluginEditor.h
...
MyReverbAudioProcessor& processor;
Slider RoomSize;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyReverbAudioProcessorEditor)
...

我们已经知道了 Reverb::Parameters 是有默认值的,于是我们调整推子的位置到默认的参数的地方。

然后写推子监听方法:当推子位置改变时调节 reverbParameters 结构体,并把它应用到 reverbInstance 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// PluginEditor.cpp
...
MyReverbAudioProcessorEditor::MyReverbAudioProcessorEditor (MyReverbAudioProcessor& p)
: AudioProcessorEditor (&p), processor (p)
{
RoomSize.setRange(0, 1, 0.01);
RoomSize.setBounds(0, 0, 300, 50);
RoomSize.setSliderStyle(Slider::SliderStyle::LinearHorizontal);
RoomSize.setTextBoxStyle(Slider::TextBoxBelow,true,100,50);

RoomSize.setValue(processor.reverbParameters.roomSize);
RoomSize.onValueChange = [this]
{
processor.reverbParameters.roomSize = RoomSize.getValue();
processor.reverbInstance.setParameters(processor.reverbParameters);
};

addAndMakeVisible(RoomSize);
setSize (400, 300);
}
...

 

这里 []{} 也就是 [](){}, C++ 中 Lambda 表达式的小括号 ‘()’ 没有参数时可以省略

 

5.验证 & 反馈调节

 

OK 没有任何问题

点击这里下载源码

课程进度80%

上海外滩 – StudioEIM // MapleStory
  1. 上海外滩 – StudioEIM // MapleStory
  2. 神木村 – StudioEIM // MapleStory
  3. MapleStory – StudioEIM // MapleStory
  4. Pantheon – StudioEIM // MapleStory
  5. 逐梦飞翔 – StudioEIM // MapleStory
  6. 魔法密林 – StudioEIM // MapleStory