EQ!使用JUCE设计一个简单的低通&高通滤波器/均衡器
|字数总计:2.4k|阅读时长:11分钟|阅读量:
前言
阅读此教程需要一定的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
| 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
| 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
| 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
| juce::AudioProcessorEditor* EqTutorialAudioProcessor::createEditor() {
return new juce::GenericAudioProcessorEditor (*this); }
|
当然,此时你移动slider不会有任何事情发生。
设置滤波器系数
我们使用下列structures和functions来从apvts中获取参数。
1 2 3 4 5 6 7 8
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| juce::AudioProcessorEditor* EqTutorialAudioProcessor::createEditor() { return new EqTutorialAudioProcessorEditor (*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
| #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
| #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)); 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
| #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;
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
| 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
| 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
| void EqTutorialAudioProcessorEditor::paint (juce::Graphics& g) { g.fillAll (black); }
|
设定NumberBox的位置和大小:
1 2 3 4 5 6 7 8 9
| 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
| 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
| 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效果器。如果你对其中的代码有优化或建议,欢迎评论或私信。感谢您能阅读到这里!