前言

阅读此教程需要一定的C++和JUCE基础,如果没有可以翻阅我的其他文章和视频教程。
核心内容为JUCE的DSP模块的使用以及其基本的书写格式,Reverb模块的使用,自制旋钮的设计。
Github原地址:https://github.com/szkkng/SimpleReverb
我的Bilibili频道:香芋派Taro
我的公众号:香芋派的烘焙坊
我的音频技术交流群:1136403177
我的个人微信:JazzyTaroPie

建立项目

打开Projucer,创建一个新的项目叫做SimpleReverb,记得对juce_dsp打上勾,以此来在项目中使用JUCE的DSP库。

DSP

APVTS

我十分推荐使用APVTS(AudioProcessorValueTreeState)来管理各种参数,因为这样做会比使用传统的方法简单很多。
首先如下来创建一个APVTS对象:

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

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

在createParameter()中放置你所有想要在APVTS中管理的对象。在这个项目中我们将要设计一个混响效果器,所以我们会用到juce::dsp::Reverb::Parameters(juce::Reverb::Parameters)
用到其中的参数有:

  • roomSize
  • damping
  • wetLevel
  • dryLevel
  • width
  • freezeMode

这些参数可以一一对应一个旋钮,但是将Dry和Wet这两个参数分拆成两个旋钮是非常不方便的(其实就是干湿比,可以发现基本我们使用的所有插件就没有将这两个参数分开的),所以我们把这两个参数合并为一个Dry/Wet旋钮。与此同时,我个人认为以百分比的形式展示参数会更好。
APVTS中的添加具体如下:

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
juce::AudioProcessorValueTreeState::ParameterLayout SimpleReverbAudioProcessor::createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;

layout.add (std::make_unique<juce::AudioParameterFloat> ("Room Size",
"Room Size",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 1.0f),
0.5f,
juce::String(),
juce::AudioProcessorParameter::genericParameter,
[](float value, int) {
if (value * 100 < 10.0f)
return juce::String (value * 100, 2);
else if (value * 100 < 100.0f)
return juce::String (value * 100, 1);
else
return juce::String (value * 100, 0); },
nullptr));

layout.add (std::make_unique<juce::AudioParameterFloat> ("Damping",
"Damping",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 1.0f),
0.5f,
juce::String(),
juce::AudioProcessorParameter::genericParameter,
[](float value, int) {
if (value * 100 < 10.0f)
return juce::String (value * 100, 2);
else if (value * 100 < 100.0f)
return juce::String (value * 100, 1);
else
return juce::String (value * 100, 0); },
nullptr));


layout.add (std::make_unique<juce::AudioParameterFloat> ("Width",
"Width",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 1.0f),
0.5f,
juce::String(),
juce::AudioProcessorParameter::genericParameter,
[](float value, int) {
if (value * 100 < 10.0f)
return juce::String (value * 100, 2);
else if (value * 100 < 100.0f)
return juce::String (value * 100, 1);
else
return juce::String (value * 100, 0); },
nullptr));

layout.add (std::make_unique<juce::AudioParameterFloat> ("Dry/Wet",
"Dry/Wet",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 1.0f),
0.5f,
juce::String(),
juce::AudioProcessorParameter::genericParameter,
[](float value, int) {
if (value * 100 < 10.0f)
return juce::String (value * 100, 2);
else if (value * 100 < 100.0f)
return juce::String (value * 100, 1);
else
return juce::String (value * 100, 0); },
nullptr));

layout.add (std::make_unique<juce::AudioParameterBool> ("Freeze", "Freeze", false));

return layout;
}

juce::dsp::Reverb

现在APVTS已经准备好了,我们将要开始设计混响部分。首先让我们创建一个juce::dsp::Reverb::Parameters对象,这个在上文稍稍介绍过。同时,创建两个juce::dsp::Reverb对象来支持双声道立体声:

1
2
3
4
5
6
7
8
9
class SimpleReverbAudioProcessor  : public juce::AudioProcessor
{
・・・
private:
juce::dsp::Reverb::Parameters params;
juce::dsp::Reverb leftReverb, rightReverb;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SimpleReverbAudioProcessor)
};

接着,准备一个ProcessSpec对象来存储一些必要的信息来初始化你已经创建的Reverb对象:

1
2
3
4
5
6
7
8
9
10
11
void SimpleReverbAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
juce::dsp::ProcessSpec spec;

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

leftReverb.prepare (spec);
rightReverb.prepare (spec);
}

然后我们开始设计音频处理的部分。正如之前说的那样,我们要把那两个参数合并成一个Dry/Wet旋钮,所以我们会用1-wet的方式得到dry的值,具体如下:

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
void SimpleReverbAudioProcessor::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());

params.roomSize = *apvts.getRawParameterValue ("Room Size");
params.damping = *apvts.getRawParameterValue ("Damping");
params.width = *apvts.getRawParameterValue ("Width");
params.wetLevel = *apvts.getRawParameterValue ("Dry/Wet");
params.dryLevel = 1.0f - *apvts.getRawParameterValue ("Dry/Wet");
params.freezeMode = *apvts.getRawParameterValue ("Freeze");

leftReverb.setParameters (params);
rightReverb.setParameters (params);

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);

leftReverb.process (leftContext);
rightReverb.process (rightContext);
}

至此,dsp部分的设计已经完成。

UI

CustomLookAndFeel

首先,我们将要定制LookAndFeel,这是我们UI元素的基础。打开Projucer并创建一个new header and cpp file。
其中头文件的具体设计如下:

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
#pragma once

#include <JuceHeader.h>

class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
CustomLookAndFeel();
~CustomLookAndFeel();

juce::Slider::SliderLayout getSliderLayout (juce::Slider& slider) override;

void drawRotarySlider (juce::Graphics&, int x, int y, int width, int height,
float sliderPosProportional, float rotaryStartAngle,
float rotaryEndAngle, juce::Slider&) override;

juce::Label* createSliderTextBox (juce::Slider& slider) override;

juce::Font getTextButtonFont (juce::TextButton&, int buttonHeight) override;

void drawButtonBackground (juce::Graphics& g, juce::Button& button,
const juce::Colour& backgroundColour,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown) override;

private:
juce::Colour blue = juce::Colour::fromFloatRGBA (0.43f, 0.83f, 1.0f, 1.0f);
juce::Colour offWhite = juce::Colour::fromFloatRGBA (0.83f, 0.84f, 0.9f, 1.0f);
juce::Colour grey = juce::Colour::fromFloatRGBA (0.42f, 0.42f, 0.42f, 1.0f);
juce::Colour blackGrey = juce::Colour::fromFloatRGBA (0.2f, 0.2f, 0.2f, 1.0f);

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomLookAndFeel);
};

以下为每个函数的具体内容讲解

getSliderLayout

函数getSliderLayout()用来设定slider的基本放置位置。换句话说,它定义了slider的大小和它应该出现在哪个位置,以及与它对应的文本框应该被放置在什么位置。
这个项目的RotarySlider(旋钮)的文本框被定义在旋钮的正中间。

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
juce::Slider::SliderLayout CustomLookAndFeel::getSliderLayout (juce::Slider& slider)
{
auto localBounds = slider.getLocalBounds();

juce::Slider::SliderLayout layout;

layout.textBoxBounds = localBounds;
layout.sliderBounds = localBounds;

return layout;
}

drawRotarySlider()
在这个构造函数中,通过调用以下这些函数来配置细节。
RotarySlider::RotarySlider()
{
setSliderStyle (juce::Slider::SliderStyle::RotaryVerticalDrag);
setTextBoxStyle (juce::Slider::TextBoxBelow, true, 0, 0);
setLookAndFeel (&customLookAndFeel);
setColour (juce::Slider::rotarySliderFillColourId, blue);
setColour (juce::Slider::textBoxTextColourId, blackGrey);
setColour (juce::Slider::textBoxOutlineColourId, grey);
setVelocityBasedMode (true);
setVelocityModeParameters (0.5, 1, 0.09, false);
setRange (0.0, 100.0, 0.01);
setRotaryParameters (juce::MathConstants<float>::pi * 1.25f,
juce::MathConstants<float>::pi * 2.75f,
true);
setWantsKeyboardFocus (true);
setTextValueSuffix (" %");
onValueChange = [&]()
{
if (getValue() < 10)
setNumDecimalPlacesToDisplay (2);
else if (getValue() >= 10 && getValue() < 100)
setNumDecimalPlacesToDisplay (1);
else
setNumDecimalPlacesToDisplay (0);
};
}

paint()

当我们将鼠标放置在旋钮上的时候,这个函数将会被调用,具体如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void RotarySlider::paint (juce::Graphics& g)
{
juce::Slider::paint (g);

if (hasKeyboardFocus (false))
{
auto length = getHeight() > 15 ? 5.0f : 4.0f;
auto thick = getHeight() > 15 ? 3.0f : 2.5f;

g.setColour (findColour (juce::Slider::textBoxOutlineColourId));

// 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);
}
}

mouseDown()/mouseUp()

使得当你的鼠标点击并拖拽旋钮时消失,松开时再出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void RotarySlider::mouseDown (const juce::MouseEvent& event)
{
juce::Slider::mouseDown (event);

setMouseCursor (juce::MouseCursor::NoCursor);
}

void RotarySlider::mouseUp (const juce::MouseEvent& event)
{
juce::Slider::mouseUp (event);

juce::Desktop::getInstance().getMainMouseSource().setScreenPosition (event.source.getLastMouseDownPosition());
setMouseCursor (juce::MouseCursor::NormalCursor);
}

NameLabel

这个类被用来设定每个旋钮对应的标签。如果我们设计这个类的话,那么我们将会使用重复的代码多次,这会十分不美观。所以在Projucer中创建一个新的头文件,具体内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma once

#include <JuceHeader.h>

class NameLabel : public juce::Label
{
public:
NameLabel()
{
setFont (20.f);
setColour (juce::Label::textColourId, grey);
setJustificationType (juce::Justification::centred);
}

~NameLabel(){}

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

};

PluginEditor

现在我们已经完成了UI元素的设计,准备开始PluginEditor部分的设计。

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
#pragma once

#include <JuceHeader.h>
#include "PluginProcessor.h"
#include "CustomLookAndFeel.h"
#include "RotarySlider.h"
#include "NameLabel.h"

//==============================================================================
/**
*/
class SimpleReverbAudioProcessorEditor : public juce::AudioProcessorEditor
{
public:
SimpleReverbAudioProcessorEditor (SimpleReverbAudioProcessor&);
~SimpleReverbAudioProcessorEditor() override;

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

private:
SimpleReverbAudioProcessor& audioProcessor;

NameLabel sizeLabel,
dampLabel,
widthLabel,
dwLabel;

RotarySlider sizeSlider,
dampSlider,
widthSlider,
dwSlider;

juce::TextButton freezeButton;

juce::AudioProcessorValueTreeState::SliderAttachment sizeSliderAttachment,
dampSliderAttachment,
widthSliderAttachment,
dwSliderAttachment;

juce::AudioProcessorValueTreeState::ButtonAttachment freezeAttachment;

CustomLookAndFeel customLookAndFeel;

juce::Colour blue = juce::Colour::fromFloatRGBA (0.43f, 0.83f, 1.0f, 1.0f);
juce::Colour offWhite = juce::Colour::fromFloatRGBA (0.83f, 0.84f, 0.9f, 1.0f);
juce::Colour grey = juce::Colour::fromFloatRGBA (0.42f, 0.42f, 0.42f, 1.0f);
juce::Colour black = juce::Colour::fromFloatRGBA (0.08f, 0.08f, 0.08f, 1.0f);

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SimpleReverbAudioProcessorEditor)
};

SimpleReverbAudioProcessorEditor()

构造函数部分如下:

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
SimpleReverbAudioProcessorEditor::SimpleReverbAudioProcessorEditor (SimpleReverbAudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p),
sizeSliderAttachment (audioProcessor.apvts, "Room Size", sizeSlider),
dampSliderAttachment (audioProcessor.apvts, "Damping", dampSlider),
widthSliderAttachment (audioProcessor.apvts, "Width", widthSlider),
dwSliderAttachment (audioProcessor.apvts, "Dry/Wet", dwSlider),
freezeAttachment (audioProcessor.apvts, "Freeze", freezeButton)
{
juce::LookAndFeel::getDefaultLookAndFeel().setDefaultSansSerifTypefaceName ("Avenir Next Medium");

setSize (500, 250);
setWantsKeyboardFocus (true);

sizeLabel.setText ("Size", juce::NotificationType::dontSendNotification);
sizeLabel.attachToComponent (&sizeSlider, false);

dampLabel.setText ("Damp", juce::NotificationType::dontSendNotification);
dampLabel.attachToComponent (&dampSlider, false);

widthLabel.setText ("Width", juce::NotificationType::dontSendNotification);
widthLabel.attachToComponent (&widthSlider, false);

dwLabel.setText ("Dry/Wet", juce::NotificationType::dontSendNotification);
dwLabel.attachToComponent (&dwSlider, false);

freezeButton.setButtonText (juce::String (juce::CharPointer_UTF8 ("∞")));
freezeButton.setClickingTogglesState (true);
freezeButton.setLookAndFeel (&customLookAndFeel);
freezeButton.setColour (juce::TextButton::buttonColourId, juce::Colours::transparentWhite);
freezeButton.setColour (juce::TextButton::buttonOnColourId, juce::Colours::transparentWhite);
freezeButton.setColour (juce::TextButton::textColourOnId, blue);
freezeButton.setColour (juce::TextButton::textColourOffId, grey);

addAndMakeVisible (sizeSlider);
addAndMakeVisible (dampSlider);
addAndMakeVisible (widthSlider);
addAndMakeVisible (dwSlider);
addAndMakeVisible (freezeButton);
}

paint()

用烟灰色填满背景,并打上”Simple Reverb”字样:

1
2
3
4
5
6
7
8
void SimpleReverbAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (black);

g.setFont (30);
g.setColour (offWhite);
g.drawText ("Simple Reverb", 150, 0, 200, 75, juce::Justification::centred);
}

resized()

每个元素的放置位置和大小:

1
2
3
4
5
6
7
8
void SimpleReverbAudioProcessorEditor::resized()
{
sizeSlider.setBounds (30, 120, 60, 60);
dampSlider.setBounds (125, 120, 60, 60);
widthSlider.setBounds (315, 120, 60, 60);
dwSlider.setBounds (410, 120, 60, 60);
freezeButton.setBounds (210, 120, 80, 40);
}

运行尝试

总结

在这篇教程中,我介绍了如何去通过JUCE DSP模块去设计一个简单的混响效果器。我十分推荐这个模块,因为它能让你很快的创建一个插件。
如果有更加高效的方法来实现这些,欢迎评论。感谢您能阅读到这里!