前言

阅读此教程需要一定的C++和JUCE基础,如果没有可以翻阅我以往的视频和文章。
核心内容为EQ效果器的设计。
Github地址:https://github.com/TaroPie0224/EqTutorial(建议先gitclone后对照学习)
我的Bilibili频道:香芋派Taro
我的公众号:香芋派的烘焙坊
我的音频技术交流群:1136403177
我的个人微信:JazzyTaroPie

在这篇教程中,我们将会带大家设计一个非常简单的EQ效果器。他仅有两个滤波器,一个高通滤波器,一个低通滤波器。你可以通过它来控制截止频率和Q值。让我们开始吧!

准备

创建一个新的plugin项目,名字任意。

记得添加dsp模块:

如果以上的步骤都没做错的话,左侧的Moudles看起来是这样子的:

算法

配置参数

首先,打开 PluginProcessor.h 准备一个 AudioProcessorValueTreeState(apvts) 对象,然后往其中添加我们所需要的参数。这部分不再细讲,不明白的同学可以回去翻看之前的教程或者回看我Gain效果器制作的那期视频。「音频编程」手把手带你写一个增益效果器(Gain)|VST AU|JUCE框架教程

1
2
3
4
5
6
7
class EqualizerTutorialAudioProcessor  : public juce::AudioProcessor
{
public:
・・・
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();

juce::AudioProcessorValueTreeState apvts { *this, nullptr, "Parameters", createParameterLayout() };

在对应cpp文件中这样定义 **createParameterLayout()**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
juce::AudioProcessorValueTreeState::ParameterLayout EqTutorialAudioProcessor::createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;

layout.add (std::make_unique<juce::AudioParameterFloat> ("LowCut Freq",
"LowCut Freq",
juce::NormalisableRange<float> (20.f, 20000.f, 1.f, 0.25f),
20.f,
juce::String(),
juce::AudioProcessorParameter::genericParameter,
[](float value, int) { return (value < 1000.0f) ?
juce::String (value, 0) + " Hz" :
juce::String (value / 1000.0f, 1) + " kHz"; },
nullptr));

layout.add (std::make_unique<juce::AudioParameterFloat> ("LowCut Quality",
"LowCut Quality",
juce::NormalisableRange<float> (0.1f, 10.f, 0.01f, 0.25f),
0.71f));

layout.add (std::make_unique<juce::AudioParameterFloat> ("HighCut Freq",
"HighCut Freq",
juce::NormalisableRange<float> (20.f, 20000.f, 1.f, 0.25f),
20000.f,
juce::String(),
juce::AudioProcessorParameter::genericParameter,
[](float value, int) { return (value < 1000.0f) ?
juce::String (value, 0) + " Hz" :
juce::String (value / 1000.0f, 1) + " kHz"; },
nullptr));

layout.add (std::make_unique<juce::AudioParameterFloat> ("HighCut Quality",
"HighCut Quality",
juce::NormalisableRange<float> (0.1f, 10.f, 0.01f, 0.25f),
0.71f));

return layout;
}

通过这种方式,我们使用AudioParameterFloat来设定了两个滤波器需要的一系列参数。上下限由人耳的听觉范围决定,分别为20Hz和20kHz,也是两个滤波器的默认值。

设计滤波器

我们将要创建一个支持立体声的滤波器,所以需要左右两个声道。

1
2
3
4
5
6
7
8
9
//PluginProcessor.h
class EqTutorialAudioProcessor : public juce::AudioProcessor
{
・・・
private:
using Filter = juce::dsp::IIR::Filter<float>;
using MonoChain = juce::dsp::ProcessorChain<Filter, Filter>;

MonoChain leftChain, rightChain;

接着,使用ProcessSpec设计prepareToPlay() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
//PluginProceesor.cpp
void EqTutorialAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
juce::dsp::ProcessSpec spec;

spec.maximumBlockSize = samplesPerBlock;
spec.numChannels = 1;
spec.sampleRate = sampleRate;

leftChain.prepare(spec);
rightChain.prepare(spec);
}

准备一个AudioBlock类的对象,设计一个channel来让左右两个滤波器都进行音频处理工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//PluginProcessor.cpp
void EqualizerTutorialAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();

for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear (i, 0, buffer.getNumSamples());

juce::dsp::AudioBlock<float> block (buffer);

auto leftBlock = block.getSingleChannelBlock (0);
auto rightBlock = block.getSingleChannelBlock (1);

juce::dsp::ProcessContextReplacing<float> leftContext (leftBlock);
juce::dsp::ProcessContextReplacing<float> rightContext (rightBlock);

leftChain.process (leftContext);
rightChain.process (rightContext);
}

通过在createEditor()中添加以下这句话可以自动生成基础的UI界面进行测试。

1
2
3
4
5
6
//PluginProcessror.cpp
juce::AudioProcessorEditor* EqTutorialAudioProcessor::createEditor()
{
// return new EqTutorialAudioProcessorEditor (*this);
return new juce::GenericAudioProcessorEditor (*this);
}

当然,此时你移动slider不会有任何事情发生。

设置滤波器系数

我们使用下列structures和functions来从apvts中获取参数。

1
2
3
4
5
6
7
8
//PluginProcessor.h
struct ChainSettings
{
float lowCutFreq { 0 }, highCutFreq { 0 };
float lowCutQuality { 0.71f }, highCutQuality { 0.71f };
};

ChainSettings getChainSettings (juce::AudioProcessorValueTreeState& apvts);

定义部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
//PluginProcessor.cpp
ChainSettings getChainSettings (juce::AudioProcessorValueTreeState& apvts)
{
ChainSettings settings;

settings.lowCutFreq = apvts.getRawParameterValue ("LowCut Freq")->load();
settings.lowCutQuality = apvts.getRawParameterValue ("LowCut Quality")->load();
settings.highCutFreq = apvts.getRawParameterValue ("HighCut Freq")->load();
settings.highCutQuality = apvts.getRawParameterValue ("HighCut Quality")->load();

return settings;
}

接下来设计一个函数来更新滤波器的系数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//PluginProcessor.h
class EqTutorialAudioProcessor : public juce::AudioProcessor
{
・・・
private:
・・・
enum ChainPositions
{
LowCut,
HighCut
};

void updateLowCutFilter (const ChainSettings& chainSettings);
void updateHighCutFilter (const ChainSettings& chainSettings);
void updateFilters();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//PluginProcessor.cpp
void EqTutorialAudioProcessor::updateLowCutFilter (const ChainSettings& chainSettings)
{
auto lowCutCoefficients = juce::dsp::IIR::Coefficients<float>::makeHighPass (getSampleRate(),
chainSettings.lowCutFreq,
chainSettings.lowCutQuality);

*leftChain.get<ChainPositions::LowCut>().coefficients = *lowCutCoefficients;
*rightChain.get<ChainPositions::LowCut>().coefficients = *lowCutCoefficients;
}

void EqTutorialAudioProcessor::updateHighCutFilter (const ChainSettings& chainSettings)
{
auto highCutCoefficients = juce::dsp::IIR::Coefficients<float>::makeLowPass (getSampleRate(),
chainSettings.highCutFreq,
chainSettings.highCutQuality);

*leftChain.get<ChainPositions::HighCut>().coefficients = *highCutCoefficients;
*rightChain.get<ChainPositions::HighCut>().coefficients = *highCutCoefficients;
}

由于低切等于高通,所以用到了makeHighPass()。同理由于高切等于低通,所以用到了makeLowPass()
我们将
updateLowCutFilter()
updateHighCutFilter() 合并到一个函数**updateFilters()**,这样以后我们每次只需要调用它即可。

1
2
3
4
5
6
7
8
//PluginProcessor.cpp
void EqTutorialAudioProcessor::updateFilters()
{
auto chainSettings = getChainSettings (apvts);

updateLowCutFilter (chainSettings);
updateHighCutFilter (chainSettings);
}

把这个函数添加到prepareToPlay()processBlock() 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//PluginProcessor.cpp
void EqTutorialAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
juce::dsp::ProcessSpec spec;

spec.maximumBlockSize = samplesPerBlock;
spec.numChannels = 1;
spec.sampleRate = sampleRate;

leftChain.prepare (spec);
rightChain.prepare (spec);

updateFilters();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//PluginProcessor.cpp
void EqTutorialAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();

for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear (i, 0, buffer.getNumSamples());

updateFilters();

juce::dsp::AudioBlock<float> block (buffer);

auto leftBlock = block.getSingleChannelBlock (0);
auto rightBlock = block.getSingleChannelBlock (1);

juce::dsp::ProcessContextReplacing<float> leftContext (leftBlock);
juce::dsp::ProcessContextReplacing<float> rightContext (rightBlock);

leftChain.process (leftContext);
rightChain.process (rightContext);
}

通过以上这些步骤,此时的滤波器应该已经能正常工作了,我们可以使用JUCE自带的AudioPluginHost来进行测试。

UI

现在我们开始设计UI部分,首先我们先将之前在createEditor中添加的自动生成UI改回去。

1
2
3
4
5
6
//PluginProcessor.cpp
juce::AudioProcessorEditor* EqTutorialAudioProcessor::createEditor()
{
return new EqTutorialAudioProcessorEditor (*this);
// return new juce::GenericAudioProcessorEditor (*this);
}

此时编译运行你应该能看到默认的 “Hello World!”

设计NumberBox

NumberBox的设计在这篇文章中有详细说明(之后也会翻译):
https://taropie0224.github.io/2022/01/11/这也是Slider?使用JUCE设计NumberBox来交互调整参数/
所以这里就简略说明一些步骤。
打开Projucer,在Source目录下点击Add New CPP & Header File,命名为NumberBox

之后显示应该如下:

代码部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//NumberBox.h
#pragma once

#include <JuceHeader.h>
#include "CustomLookAndFeel.h"

class NumberBox : public juce::Slider
{
public:
NumberBox();
~NumberBox() override;

void paint (juce::Graphics&) override;

void mouseDown (const juce::MouseEvent& event) override;
void mouseUp (const juce::MouseEvent& event) override;

void focusGained (juce::Component::FocusChangeType) override;
void focusLost (juce::Component::FocusChangeType) override;

bool getLockedOnState ();
void setLockedOnState (bool state);

private:
bool isLockedOn = false;

CustomLookAndFeel customLookAndFeel;

juce::Colour grey = juce::Colour::fromFloatRGBA(0.42, 0.42, 0.42, 1.0);

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NumberBox)
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//NumberBox.cpp
#include <JuceHeader.h>
#include "NumberBox.h"

NumberBox::NumberBox()
{
setSliderStyle (juce::Slider::LinearBarVertical);
setLookAndFeel(&customLookAndFeel);
setColour (juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite);
setColour (juce::Slider::trackColourId, juce::Colours::transparentWhite);
setTextBoxIsEditable (false);
setVelocityBasedMode (true);
setVelocityModeParameters (0.5, 1, 0.09, false);
setRange (0, 100, 0.01);
setWantsKeyboardFocus (true);
onValueChange = [&]()
{
if (getValue() < 10)
setNumDecimalPlacesToDisplay(2);
else if (getValue() >= 10 && getValue() < 100)
setNumDecimalPlacesToDisplay(1);
else
setNumDecimalPlacesToDisplay(0);
};
}

NumberBox::~NumberBox()
{
}

void NumberBox::paint (juce::Graphics& g)
{
if (isLockedOn)
{
auto length = getHeight() > 15 ? 4 : 3;
auto thick = getHeight() > 15 ? 3.0 : 2.5;

g.setColour (findColour (juce::Slider::textBoxTextColourId));
// fromX fromY toX toY
g.drawLine (0, 0, 0, length, thick);
g.drawLine (0, 0, length, 0, thick);
g.drawLine (0, getHeight(), 0, getHeight() - length, thick);
g.drawLine (0, getHeight(), length, getHeight(), thick);
g.drawLine (getWidth(), getHeight(), getWidth() - length, getHeight(), thick);
g.drawLine (getWidth(), getHeight(), getWidth(), getHeight() - length, thick);
g.drawLine (getWidth(), 0, getWidth() - length, 0, thick);
g.drawLine (getWidth(), 0, getWidth(), length, thick);
}
}

void NumberBox::mouseDown (const juce::MouseEvent& event)
{
juce::Slider::mouseDown (event);
setMouseCursor (juce::MouseCursor::NoCursor);
}

void NumberBox::mouseUp (const juce::MouseEvent& event)
{
juce::Slider::mouseUp (event);
juce::Desktop::getInstance().getMainMouseSource().setScreenPosition(event.source.getLastMouseDownPosition());
setMouseCursor (juce::MouseCursor::NormalCursor);
}

void NumberBox::focusGained (juce::Component::FocusChangeType)
{
setLockedOnState (true);
}

void NumberBox::focusLost (juce::Component::FocusChangeType)
{
setLockedOnState (false);
}

bool NumberBox::getLockedOnState ()
{
return isLockedOn;
}

void NumberBox::setLockedOnState (bool state)
{
isLockedOn = state;
repaint();
}

配置PluginEditor

现在如下所示创建一个NumberBox 对象,注意不要忘记在开头引入NumberBox.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//PluginEditor.h
#pragma once

#include <JuceHeader.h>
#include "PluginProcessor.h"
#include "NumberBox.h"

class EqTutorialAudioProcessorEditor : public juce::AudioProcessorEditor
{
public:
EqTutorialAudioProcessorEditor (EqTutorialAudioProcessor&);
~EqTutorialAudioProcessorEditor() override;

//==============================================================================
void paint (juce::Graphics&) override;
void resized() override;

//void mouseDown (const juce::MouseEvent& event) override

private:
EqTutorialAudioProcessor& audioProcessor;

NumberBox lowCutFreqBox;
NumberBox lowCutQualityBox;

NumberBox highCutFreqBox;
NumberBox highCutQualityBox;

juce::Colour blue = juce::Colour::fromFloatRGBA (0.4, 0.8, 1.0, 1.0);
juce::Colour yellow = juce::Colour::fromFloatRGBA (1.0, 0.7, 0.2, 1.0);
juce::Colour black = juce::Colour::fromFloatRGBA (0.08, 0.08, 0.08, 1.0);

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EqTutorialAudioProcessorEditor)
};

定义部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//PluginEditor.cpp
EqTutorialAudioProcessorEditor::EqTutorialAudioProcessorEditor (EqTutorialAudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p)
{
setSize (300, 150);
setWantsKeyboardFocus (true);

lowCutFreqBox.setColour (juce::Slider::textBoxTextColourId, blue);
lowCutQualityBox.setColour (juce::Slider::textBoxTextColourId, blue.darker(0.3));

highCutFreqBox.setColour (juce::Slider::textBoxTextColourId, yellow);
highCutQualityBox.setColour (juce::Slider::textBoxTextColourId, yellow.darker(0.3));

addAndMakeVisible (lowCutFreqBox);
addAndMakeVisible (lowCutQualityBox);
addAndMakeVisible (highCutFreqBox);
addAndMakeVisible (highCutQualityBox);
}
1
2
3
4
5
6
7
8
9
//PluginEditor.cpp
void EqTutorialAudioProcessorEditor::resized()
{
lowCutFreqBox.setBounds (35, getHeight() / 2 - 20, 80, 20);
lowCutQualityBox.setBounds (35, getHeight() / 2 + 10, 80, 20);

highCutFreqBox.setBounds (185, getHeight() / 2 - 20, 80, 20);
highCutQualityBox.setBounds (185, getHeight() / 2 + 10, 80, 20);
}

把界面背景色设为黑色:

1
2
3
4
5
//PluginEditor.cpp
void EqTutorialAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (black);
}

设定NumberBox的位置和大小:

1
2
3
4
5
6
7
8
9
//PluginEditor.cpp
void EqTutorialAudioProcessorEditor::resized()
{
lowCutFreqBox.setBounds (35, getHeight() / 2 - 20, 80, 20);
lowCutQualityBox.setBounds (35, getHeight() / 2 + 10, 80, 20);

highCutFreqBox.setBounds (185, getHeight() / 2 - 20, 80, 20);
highCutQualityBox.setBounds (185, getHeight() / 2 + 10, 80, 20);
}

尝试编译运行,由于此时NumberBox还没有和APVTS进行连接,所以看上去应该如下:

将算法与UI进行连接

把NumberBox连接到参数

我们使用SliderAttachment来连接二者。注意,必须在NumberBox对象的声明下方声明Attachment对象,否则就寄了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//PluginEditor.h
class EqTutorialAudioProcessorEditor : public juce::AudioProcessorEditor
{
・・・
private:
EqTutorialAudioProcessor& audioProcessor;

NumberBox lowCutFreqBox;
NumberBox lowCutQualityBox;
NumberBox highCutFreqBox;
NumberBox highCutQualityBox;

using Attachment = juce::AudioProcessorValueTreeState::SliderAttachment;

Attachment lowCutFreqAttachment,
lowCutQualityAttachment,
highCutFreqAttachment,
highCutQualityAttachment;

如下所示,apvts对象为第一个参数,parameter ID为第二个参数,NumberBox对象为第三个参数:

1
2
3
4
5
6
7
8
9
10
11
//PluginEditor.cpp
EqTutorialAudioProcessorEditor::EqTutorialAudioProcessorEditor (EqTutorialAudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p),
lowCutFreqAttachment (audioProcessor.apvts, "LowCut Freq", lowCutFreqBox),
lowCutQualityAttachment (audioProcessor.apvts, "LowCut Quality", lowCutQualityBox),
highCutFreqAttachment (audioProcessor.apvts, "HighCut Freq", highCutFreqBox),
highCutQualityAttachment (audioProcessor.apvts, "HighCut Quality", highCutQualityBox)

{
・・・
}

编译并进行测试

总结

在这篇教程中,我们介绍了如何设计一个简单的EQ效果器。如果你对其中的代码有优化或建议,欢迎评论或私信。感谢您能阅读到这里!