├── .gitattributes ├── README.md ├── 04-保存插件状态.md ├── 01-Hello! Projucer!(Win&Mac环境配置).md ├── 02-创建插件的参数和界面(APVTS与界面自动生成).md └── 03-编写你的第一个效果器(Gain).md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | [01-Hello! Projucer!(Win&Mac环境配置)](https://github.com/TaroPie1214/JUCE-101/blob/main/01-Hello!%20Projucer!%EF%BC%88Win%26Mac%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE%EF%BC%89.md) 4 | 5 | [02-创建插件的参数和界面(APVTS与界面自动生成)](https://github.com/TaroPie1214/JUCE-101/blob/main/02-%E5%88%9B%E5%BB%BA%E6%8F%92%E4%BB%B6%E7%9A%84%E5%8F%82%E6%95%B0%E5%92%8C%E7%95%8C%E9%9D%A2%EF%BC%88APVTS%E4%B8%8E%E7%95%8C%E9%9D%A2%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90%EF%BC%89.md) 6 | 7 | [03-编写你的第一个效果器(Gain)](https://github.com/TaroPie1214/JUCE-101/blob/main/03-%E7%BC%96%E5%86%99%E4%BD%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%95%88%E6%9E%9C%E5%99%A8%EF%BC%88Gain%EF%BC%89.md) 8 | 9 | [04-保存插件状态](https://github.com/TaroPie1214/JUCE-101/blob/main/04-%E4%BF%9D%E5%AD%98%E6%8F%92%E4%BB%B6%E7%8A%B6%E6%80%81.md) 10 | -------------------------------------------------------------------------------- /04-保存插件状态.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 你可以在这里找到本章中的相关代码: 4 | 5 | [TaroStudio/TaroGain at main · TaroPie1214/TaroStudio (github.com)](https://github.com/TaroPie1214/TaroStudio/tree/main/TaroGain) 6 | 7 | 虽然这部分内容非常的简短,但还是单开一章方便查阅吧。 8 | 9 | 通过以上三章的内容,现在应该已经完成了Gain效果器的创建,但你可能发现,此时在DAW中保存工程时并不会保存插件中参数的当前值,每次重新打开工程都会把参数重置回默认值。那么怎么保存插件的状态呢? 10 | 11 | 官网上有一篇相关的tutorial,涉及到xml的转换。 12 | 13 | ![未命名1696994770.png](https://cdn.jsdelivr.net/gh/TaroPie1214/blogImage@master/img/%E6%9C%AA%E5%91%BD%E5%90%8D1696994770.png) 14 | 15 | 但由于我们有且仅有使用了APVTS来管理我们的参数,那么我们不妨直接存入和读取ValueTree的状态。 16 | 17 | # getStateInformation 18 | 19 | ```cpp 20 | // PluginProcessor.cpp 21 | void TaroGainAudioProcessor::getStateInformation (juce::MemoryBlock& destData) 22 | { 23 | juce::MemoryOutputStream stream(destData, false); 24 | apvts.state.writeToStream(stream); 25 | } 26 | ``` 27 | 28 | # setStateInformation 29 | 30 | ```cpp 31 | // PluginProcessor.cpp 32 | void TaroGainAudioProcessor::setStateInformation (const void* data, int sizeInBytes) 33 | { 34 | juce::ValueTree tree = juce::ValueTree::readFromData(data, sizeInBytes); 35 | 36 | if (tree.isValid()) { 37 | apvts.state = tree; 38 | } 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /01-Hello! Projucer!(Win&Mac环境配置).md: -------------------------------------------------------------------------------- 1 | # 获取JUCE 2 | 3 | 有两种方式可以获取JUCE(Cmake/Projucer),初学的朋友更推荐使用官方的构建工具Projucer 4 | 5 | Projucer: [Download - JUCE](https://juce.com/download/) 6 | 7 | Git: [juce-framework/JUCE: JUCE is an open-source cross-platform C++ application framework for desktop and mobile applications, including VST, VST3, AU, AUv3, RTAS and AAX audio plug-ins. (github.com)](https://github.com/juce-framework/JUCE) 8 | 9 | # Projucer 10 | 11 | ## Global Paths配置 12 | 13 | 下载完成后会得到一个JUCE文件夹,对于Windows用户,如果你不嫌麻烦在之后配置Global Paths,且你并没有多年的开发经验的话,**我强烈建议你直接将这整个文件夹放在C盘的根目录下**,这是由于Projucer的默认设置就是指向这个位置的 14 | 15 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-100919.png) 16 | 17 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-100548.png) 18 | 19 | Mac对应的默认位置: 20 | 21 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-101136.png) 22 | 23 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-101129.png) 24 | 25 | 如果你不想放在默认位置,请修改Path to JUCE和JUCE Modules指向你的自定义路径 26 | 27 | ## 创建第一个JUCE工程 28 | 29 | 点击左上角的FIle选择New Project... 30 | 31 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-102305.png) 32 | 33 | 由于本系列教程主要以音频效果器插件为例,所以选择Projucer Plugin-In中的Basic模板 34 | 35 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-102421.png) 36 | 37 | 输入你的项目名称,Modules暂时不用动,Path to Modules在上文我们已经配置好了,Exporters根据系统选择(Win->VS,Mac->Xcode),File Creation Options默认即可 38 | 39 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-102737.png) 40 | 41 | 点击Create Project,选择生成路径后即可完成项目的创建,此时点击右上方的IDE图标即可打开对应IDE 42 | 43 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-103143.png) 44 | 45 | # 编译与运行 46 | 47 | ## Windows 48 | 49 | 点击上方的调试器开始编译 50 | 51 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-103652.png) 52 | 53 | 会默认编译并打开standalone版本,如果一切顺利,你会看到这个界面 54 | 55 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-104438.png) 56 | 57 | 停止调试,右键解决方案资源管理器中的VST解决方案并生成 58 | 59 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-104638.png) 60 | 61 | 如果编译成功,我们可以在下图路径中找到结果 62 | 63 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-104738.png) 64 | 65 | 将其复制到 *C:\Program Files\Common Files\VST3* 下,这是一般DAW的VST3默认扫描位置 66 | 67 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-105252.png) 68 | 69 | 然后就可以在DAW的调音台中打开我们刚刚编译的插件了 70 | 71 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-105333.png) 72 | 73 | ## Mac 74 | 75 | 在Xcode中,我们也先尝试编译Standalone版本 76 | 77 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-105846.jpg) 78 | 79 | 会得到跟Windows端相同的内容 80 | 81 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-110055.png) 82 | 83 | 更换解决方案,这里我们以AU为例 84 | 85 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-110208.png) 86 | 87 | 编译完成后,跟Windows不同的是,在项目的build目录下生成的是替身,实际结果会直接build到系统统一放置**用户插件**的路径,如下 88 | 89 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-110934.png) 90 | 91 | 但此时Logic会有概率扫描不到该插件,如果发生这种情况,请复制到**系统插件**路径下 92 | 93 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-111255.png)DAW扫描到插件后,也就可以正常挂载了 94 | 95 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/2023/04/03/20230403-110919.png) 96 | 97 | # 总结 98 | 99 | 在本章中,我们学习了在Win和Mac下使用Projucer并完成工程的基础配置,成功编译运行。 100 | 101 | 在下一章中,我们将学习如何将我们想要控制的参数添加到插件中,并自动生成对应的交互界面。 102 | -------------------------------------------------------------------------------- /02-创建插件的参数和界面(APVTS与界面自动生成).md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 你可以在这里找到本章中的相关代码: 4 | 5 | [TaroStudio/TaroCompressor at main · TaroPie1214/TaroStudio (github.com)](https://github.com/TaroPie1214/TaroStudio/tree/main/TaroCompressor) 6 | 7 | # 参数管理 8 | 9 | 在我们设计各类插件时,不可避免的会面对众多参数的管理问题(比如下图中一个简单的compressor就包含了Threshold,Attack,Release,Ratio等参数),对此JUCE提供了一种方法叫做AudioProcessorValueTreeState(以下简称APVTS): 10 | 11 | https://docs.juce.com/master/classAudioProcessorValueTreeState.html 12 | 13 | ![image-20230925113623287](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/202309251136375.png) 14 | 15 | # APVTS 16 | 17 | 在进入这部分内容的教程之前,请确保你已经完成了上一章的内容,即配置好了开发环境并创建了一个plugin工程。 18 | 19 | 打开PluginProcessor.h,在public部分补充以下内容: 20 | 21 | ```cpp 22 | // PluginProcessor.h 23 | public: 24 | // ... 25 | using APVTS = juce::AudioProcessorValueTreeState; 26 | static APVTS::ParameterLayout createParameterLayout(); 27 | 28 | APVTS apvts {*this, nullptr, "Parameters", createParameterLayout() }; 29 | ``` 30 | 31 | 由于AudioProcessorValueTreeState这一长串字母实在太长,我们通常用using简化成APVTS 32 | 33 | 打开PluginProcessor.cpp,实现刚刚创建的createParameterLayout()方法,这里以一个compressor的相关实现为例: 34 | 35 | ```cpp 36 | juce::AudioProcessorValueTreeState::ParameterLayout TaroCompressorAudioProcessor::createParameterLayout() 37 | { 38 | APVTS::ParameterLayout layout; 39 | 40 | using namespace juce; 41 | 42 | layout.add(std::make_unique(ParameterID { "Threshold", 1 }, 43 | "Threshold", 44 | NormalisableRange(-60, 12, 1, 1), 0)); 45 | 46 | auto attackReleaseRange = NormalisableRange(0, 500, 1, 1); 47 | 48 | layout.add(std::make_unique(ParameterID { "Attack", 1 }, "Attack", attackReleaseRange, 50)); 49 | 50 | layout.add(std::make_unique(ParameterID { "Release", 1 }, "Release", attackReleaseRange, 250)); 51 | 52 | auto choices = std::vector{ 1.5, 2, 3, 4, 5, 6, 7, 8, 10, 15, 20, 50, 100 }; 53 | juce::StringArray sa; 54 | for ( auto choice : choices ) 55 | { 56 | sa.add( juce::String(choice) ); 57 | } 58 | 59 | layout.add(std::make_unique(ParameterID { "Ratio", 1 }, "Ratio", sa, 3)); 60 | 61 | return layout; 62 | } 63 | ``` 64 | 65 | 接下来我们对以上代码进行拆解 66 | 67 | 假设我们有一个参数叫做Threshold,它的最小值为-60dB,最大值为12dB,最小步进(参数可调整的最大精度)为1dB,默认值为0dB。 68 | 69 | ```cpp 70 | layout.add(std::make_unique(ParameterID { "Threshold", 1 }, 71 | "Threshold", 72 | NormalisableRange(-60, 12, 1, 1), 0)); 73 | ``` 74 | 75 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/202309251604164.png) 76 | 77 | 我们首先来看一下AudioParameterFloat,在这里它接收了四个参数,分别是: 78 | 79 | - parameterID:参数的ID,必须唯一,我们通过这个ID在别处去修改或获取参数的值 80 | 81 | - parameterName:参数的名称,用于在交互界面上进行展示等等 82 | 83 | - normalisableRange:参数的范围,它也接受了四个参数,分别是 84 | 85 | 1. rangeStart:最小值 86 | 87 | 2. rangeEnd:最大值 88 | 89 | 3. intervalValue:步进 90 | 91 | 4. skewFactor:斜率因子 92 | 93 | - 大部分情况下都是1,如果参数绑定了一个滑块或者旋钮,那么1就表示随着滑块或旋钮的移动,参数均匀变化 94 | 95 | - 小部分情况下需要手动调整,比如该参数是EQ效果器中被衰减或增益的一个频点,那么在较低频率时参数应该变化较慢,在较高频率时,则应该变化较快,这一点在大部分EQ效果器上的横轴分布就有体现 96 | 97 | ![image-20230925161504441](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/202309251615581.png) 98 | 99 | - defaultValue:默认值 100 | 101 | 你可能还会发现,在parameterID的位置,我并没有直接给出一个字符串,而是使用了 102 | 103 | ```cpp 104 | ParameterID { "Threshold", 1 } 105 | ``` 106 | 107 | 请注意,这样的写法只在编译AU(Audio Unit)格式的插件时需要被使用,其它情况,如Standalone,VST3等则不需要这个步骤,直接给出字符串即可(当然你全都这么写上也不冲突) 108 | 109 | ``` 110 | layout.add(std::make_unique("Threshold", 111 | "Threshold", 112 | NormalisableRange(-60, 12, 1, 1), 0)); 113 | ``` 114 | 115 | 我们再来看一个AudioParameterChoice,它跟AudioParameterFloat是最常用的两种APVTS参数类型 116 | 117 | ![image-20230925164447235](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/202309251644335.png) 118 | 119 | 它也接收了四个参数,分别是: 120 | 121 | - parameterID 122 | - parameterName 123 | - choices:一个StringArray字符串数组,包含所有的选项 124 | - defaultItemIndex:默认选项的Index 125 | 126 | ```cpp 127 | auto choices = std::vector{ 1.5, 2, 3, 4, 5, 6, 7, 8, 10, 15, 20, 50, 100 }; 128 | juce::StringArray sa; 129 | for ( auto choice : choices ) 130 | { 131 | sa.add( juce::String(choice) ); 132 | } 133 | layout.add(std::make_unique(ParameterID { "Ratio", 1 }, "Ratio", sa, 3)); 134 | ``` 135 | 136 | 在这里,我们首先创建了一个juce::StringArray数组,并把所有的Ratio可选值添加到了其中,然后再以此创建了一个AudioParameterChoice并添加到了layout,其中默认选项的index为3,所以在插件初始化时,Ratio的值会默认是4(0->1.5,1->2,2->3,3->4,...) 137 | 138 | # 自动界面生成 139 | 140 | 当我们创建完APVTS之后,此时对代码进行编译,显示的依然还是默认的Hello World,因为我们并没有对PluginEditor进行任何修改。 141 | 142 | 在之后,我们会讲到如何创建自定义控件并与我们在APVTS中创建的参数进行绑定,并自由设计和摆放我们的控件。但创建控件->绑定参数->摆放控件这一过程还是略显繁琐,如果我们只是在插件的测试阶段(只需要测试它的dsp效果等),其实完全可以跳过这个步骤。 143 | 144 | ![](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/202309251700294.png) 145 | 146 | 在这里,JUCE提供可一种自动构建界面的方法。 147 | 148 | 打开PluginProcessor.cpp,找到以下内容: 149 | 150 | ```cpp 151 | juce::AudioProcessorEditor* TaroCompressorAudioProcessor::createEditor() 152 | { 153 | return new TaroCompressorAudioProcessorEditor (*this); 154 | } 155 | ``` 156 | 157 | 把它替换成: 158 | 159 | ```cpp 160 | juce::AudioProcessorEditor* TaroCompressorAudioProcessor::createEditor() 161 | { 162 | return new juce::GenericAudioProcessorEditor (*this); 163 | } 164 | ``` 165 | 166 | 由此JUCE会根据你当前在APVTS中创建的所有参数自动构建界面,自动生成对应的Slider或ComboBox,如果你按照上述的代码构建了APVTS,那么你此时应该会看到这样的界面: 167 | 168 | ![image-20230925113623287](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/202309251705638.png) 169 | 170 | 不过,由于我们并没有设计压缩器的DSP(数字信号处理)部分,所以无论你如何拖动Slider,这个效果器都处于被旁通的状态。但是不用担心,我们将马上进入DSP内容的讲解! 171 | 172 | # 总结 173 | 174 | 你可以在这里找到本章中的相关代码: 175 | 176 | [TaroPie1214/TaroStudio: A collection of all my plugins! (github.com)](https://github.com/TaroPie1214/TaroStudio/tree/main/TaroCompressor) 177 | 178 | 在本章中,我们首先学习了JUCE中常用的参数管理方式——APVTS,了解了如何向其中添加我们想要的参数,然后根据这些参数自动生成对应的界面。 179 | 180 | 在下一章中,我们将开始窥探DSP部分的内容,由此创建我们第一个效果器! 181 | -------------------------------------------------------------------------------- /03-编写你的第一个效果器(Gain).md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 在上一章中,我们已经学会了如何配置参数。在这一章中,我们将设计一个Gain效果器,通过**多种不同的实现方式**来介绍参数的获取和Real-Time效果器的DSP部分基本写作逻辑。 4 | 5 | 你可以在这里找到本章中的相关代码: 6 | 7 | [TaroStudio/TaroGain at main · TaroPie1214/TaroStudio (github.com)](https://github.com/TaroPie1214/TaroStudio/tree/main/TaroGain) 8 | 9 | # 准备工作 10 | 11 | 在Projucer中添加module:juce_dsp 12 | 13 | ![iShot2023-10-09 11.11.31](https://cdn.jsdelivr.net/gh/TaroPie0224/blogImage@main/img/202310091111021.jpg) 14 | 15 | 根据之前的教程创建好工程并配置好我们这次需要的参数——Gain: 16 | 17 | ```cpp 18 | juce::AudioProcessorValueTreeState::ParameterLayout TaroGainAudioProcessor::createParameterLayout() 19 | { 20 | APVTS::ParameterLayout layout; 21 | 22 | using namespace juce; 23 | 24 | layout.add(std::make_unique("Gain", 25 | "Gain(dB)", 26 | NormalisableRange(-10.f, 10.f, 0.1f, 1), 0.f)); 27 | 28 | return layout; 29 | } 30 | ``` 31 | 32 | 自动界面生成: 33 | 34 | ```cpp 35 | juce::AudioProcessorEditor* TaroGainAudioProcessor::createEditor() 36 | { 37 | return new juce::GenericAudioProcessorEditor(*this); 38 | } 39 | ``` 40 | 41 | # processBlock 42 | 43 | 到达Real-Time效果器最高城!processBlock! 44 | 45 | 让我们先看看它默认长什么样: 46 | 47 | ```cpp 48 | void AudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) 49 | { 50 | juce::ScopedNoDenormals noDenormals; 51 | auto totalNumInputChannels = getTotalNumInputChannels(); 52 | auto totalNumOutputChannels = getTotalNumOutputChannels(); 53 | 54 | // In case we have more outputs than inputs, this code clears any output 55 | // channels that didn't contain input data, (because these aren't 56 | // guaranteed to be empty - they may contain garbage). 57 | // This is here to avoid people getting screaming feedback 58 | // when they first compile a plugin, but obviously you don't need to keep 59 | // this code if your algorithm always overwrites all the output channels. 60 | for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) 61 | buffer.clear (i, 0, buffer.getNumSamples()); 62 | 63 | // This is the place where you'd normally do the guts of your plugin's 64 | // audio processing... 65 | // Make sure to reset the state if your inner loop is processing 66 | // the samples and the outer loop is handling the channels. 67 | // Alternatively, you can process the samples with the channels 68 | // interleaved by keeping the same state. 69 | for (int channel = 0; channel < totalNumInputChannels; ++channel) 70 | { 71 | auto* channelData = buffer.getWritePointer (channel); 72 | 73 | // ..do something to the data... 74 | } 75 | } 76 | ``` 77 | 78 | --- 79 | 80 | 首先是: 81 | 82 | ```cpp 83 | juce::ScopedNoDenormals noDenormals; 84 | ``` 85 | 86 | 它的意思其实是禁用当前作用域下的浮点数的非规格化处理,可能有点绕口,但是这能在部分情况下显著提升音频的处理速度,当然也会伴随着一些负面影响。 87 | 88 | 关于具体什么是浮点数的规格化和非规格化处理,以及它们分别有什么利弊,可以查看以下这位老师的博客,讲解的十分清晰: 89 | 90 | http://cenalulu.github.io/linux/about-denormalized-float-number/ 91 | 92 | **当然,如果你并不在乎为什么需要这行代码,我也鼓励你直接跳过来节省时间。** 93 | 94 | --- 95 | 96 | ```cpp 97 | auto totalNumInputChannels = getTotalNumInputChannels(); 98 | auto totalNumOutputChannels = getTotalNumOutputChannels(); 99 | ``` 100 | 101 | 获取了输入通道的数量和输出通道的数量。 102 | 103 | --- 104 | 105 | ```cpp 106 | for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) 107 | buffer.clear (i, 0, buffer.getNumSamples()); 108 | ``` 109 | 110 | 将多余的输出通道内容置零,以防止啸叫产生。 111 | 112 | --- 113 | 114 | ```cpp 115 | for (int channel = 0; channel < totalNumInputChannels; ++channel) 116 | { 117 | auto* channelData = buffer.getWritePointer (channel); 118 | 119 | // ..do something to the data... 120 | } 121 | ``` 122 | 123 | 遍历所有的输入通道,并获取指向该通道下buffer的头部的指针,同时也是最关键的部分。 124 | 125 | # Gain 126 | 127 | Gain(增益)效果器是什么东西应该不用再具体介绍了,我们可以通过Gain来控制音频的响度。 128 | 129 | 接下来将用四种不同的方式来实现这个效果。 130 | 131 | ## 方式一 132 | 133 | 我们先了解一下如何获取apvts里面的参数值: 134 | 135 | ```cpp 136 | float currentGain = *apvts.getRawParameterValue("Gain"); //括号里的字符需为之前提到的Parameter ID 137 | ``` 138 | 139 | 回看刚刚提到的循环——遍历所有的输入通道,并获取指向该通道下buffer的头部的指针。 140 | 141 | 现在我们在这个循环中再嵌套一层对buffer的循环,以遍历buffer中的所有sample: 142 | 143 | ```cpp 144 | // 通过buffer.getNumSamples()获取buffer里sample的数量 145 | for (int sample = 0; sample < buffer.getNumSamples(); ++sample) 146 | { 147 |     // ... 148 | } 149 | ``` 150 | 151 | 对于每个sample,我们将它乘上我们的gain: 152 | 153 | ```cpp 154 | // 注意我们的Gain定义的时候单位是dB,所以这里还需要一个转换 155 | channelData[sample] *= juce::Decibels::decibelsToGain(currentGain); 156 | ``` 157 | 158 | 整体代码如下: 159 | 160 | ```cpp 161 | void TaroGainAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) 162 | { 163 | juce::ScopedNoDenormals noDenormals; 164 | auto totalNumInputChannels = getTotalNumInputChannels(); 165 | auto totalNumOutputChannels = getTotalNumOutputChannels(); 166 | 167 | for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) 168 | buffer.clear(i, 0, buffer.getNumSamples()); 169 | 170 | float currentGain = *apvts.getRawParameterValue("Gain"); 171 | 172 | for (int channel = 0; channel < totalNumInputChannels; ++channel) 173 | { 174 | auto* channelData = buffer.getWritePointer (channel); 175 | 176 | for (int sample = 0; sample < buffer.getNumSamples(); ++sample) 177 | { 178 | channelData[sample] *= juce::Decibels::decibelsToGain(currentGain); 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | ## 方式二 185 | 186 | 在这之前我们需要先了解一下JUCE的AudioBlock: 187 | 188 | [JUCE: dsp::AudioBlock< SampleType > Class Template Reference](https://docs.juce.com/master/classdsp_1_1AudioBlock.html) 189 | 190 | 简单来说,它并不包含具体的数据,只是一个引用,我们通常用AudioBuffer来创建一个AudioBlock。在JUCE的dsp module中,block是非常关键的一部分,在之后的几个章节中,我们会经常使用它。 191 | 192 | 同理,在创建完block后,参考方式一,对block中的sample进行遍历并乘上gain。 193 | 194 | 整体代码如下: 195 | 196 | ```cpp 197 | void TaroGainAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) 198 | { 199 | juce::ScopedNoDenormals noDenormals; 200 | auto totalNumInputChannels = getTotalNumInputChannels(); 201 | auto totalNumOutputChannels = getTotalNumOutputChannels(); 202 | 203 | for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) 204 | buffer.clear(i, 0, buffer.getNumSamples()); 205 | 206 | float currentGain = *apvts.getRawParameterValue("Gain"); 207 | 208 | juce::dsp::AudioBlock block(buffer); 209 | for (int channel = 0; channel < block.getNumChannels(); ++channel) 210 | { 211 | auto* channelData = block.getChannelPointer(channel); 212 | for (int sample = 0; sample < block.getNumSamples(); ++sample) 213 | { 214 | channelData[sample] *= juce::Decibels::decibelsToGain(currentGain); 215 | } 216 | } 217 | } 218 | ``` 219 | 220 | ## 方式三 221 | 222 | 其实,JUCE的AudioBuffer还自带一个方法叫做applyGain: 223 | 224 | ![未命名1696989921.png](https://cdn.jsdelivr.net/gh/TaroPie1214/blogImage@master/img/%E6%9C%AA%E5%91%BD%E5%90%8D1696989921.png) 225 | 226 | 使用该方法,我们可以直接对整个buffer进行gain的相乘。 227 | 228 | 整体代码如下: 229 | 230 | ```cpp 231 | void TaroGainAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) 232 | { 233 | juce::ScopedNoDenormals noDenormals; 234 | auto totalNumInputChannels = getTotalNumInputChannels(); 235 | auto totalNumOutputChannels = getTotalNumOutputChannels(); 236 | 237 | for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) 238 | buffer.clear(i, 0, buffer.getNumSamples()); 239 | 240 | float currentGain = *apvts.getRawParameterValue("Gain"); 241 | 242 | buffer.applyGain(juce::Decibels::decibelsToGain(currentGain)); 243 | } 244 | ``` 245 | 246 | 备注:AudioBuffer作为JUCE的核心内容之一,JUCE为其提供了很多方法,这些方法可以在很多时候方便我们对buffer的处理,所以推荐对AudioBuffer的所有方法进行一个完整的预览,留个印象。 247 | 248 | https://docs.juce.com/master/classAudioBuffer.html 249 | 250 | ## 方式四 251 | 252 | 这个方法涉及到JUCE的DSP模块使用,在此也通过Gain来引入其基本的使用方式。 253 | 254 | 首先在PluginProcessor.h的private部分创建一个Gain: 255 | 256 | ```cpp 257 | // PluginProcessor.h 258 | class TaroGainAudioProcessor : public juce::AudioProcessor 259 | { 260 | public: 261 | // ... 262 | 263 | private: 264 | // 方式4 265 | juce::dsp::Gain gain; 266 | //============================================================================== 267 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TaroGainAudioProcessor) 268 | }; 269 | ``` 270 | 271 | 回到PluginProcessor.cpp,找到prepareToPlay。 272 | 273 | 在这里简单介绍一下prepareToPlay,它是插件在被初始化时执行的函数,当我们使用dsp modules时,我们会在其中告诉ProcessSpec当前的buffer大小,声道数量,采样率等等,并进行prepare和reset。 274 | 275 | 对于Gain来说,prepareToPlay的写法如下: 276 | 277 | ```cpp 278 | // PluginProcessor.cpp 279 | void TaroGainAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) 280 | { 281 | // 方式4 282 | // 创建spec 283 | juce::dsp::ProcessSpec spec; 284 | // 告知spec当前的buffer大小,声道数量,采样率 285 | spec.maximumBlockSize = samplesPerBlock; 286 | spec.numChannels = getTotalNumInputChannels(); 287 | spec.sampleRate = sampleRate; 288 | // 以这个spec中的信息来执行gain的处理 289 | gain.prepare(spec); 290 | gain.reset(); 291 | } 292 | ``` 293 | 294 | 最后我们再来看processBlock。 295 | 296 | 同理,还是首先根据AudioBuffer创建一个AudioBlock,然后我们使用gain的setGainDecibels方法来告知它当前的增益大小,再通过process方法来执行gain效果。 297 | 298 | 注意,在将block送入process方法时,我们还对其进行了一个ProcessContextReplacing(block)的嵌套,不要忘记这个操作。 299 | 300 | ![未命名1696992985.png](https://cdn.jsdelivr.net/gh/TaroPie1214/blogImage@master/img/%E6%9C%AA%E5%91%BD%E5%90%8D1696992985.png) 301 | 302 | 整体代码如下: 303 | 304 | ```cpp 305 | // PluginProcessor.cpp 306 | void TaroGainAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) 307 | { 308 | juce::ScopedNoDenormals noDenormals; 309 | auto totalNumInputChannels = getTotalNumInputChannels(); 310 | auto totalNumOutputChannels = getTotalNumOutputChannels(); 311 | 312 | for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) 313 | buffer.clear(i, 0, buffer.getNumSamples()); 314 | 315 | float currentGain = *apvts.getRawParameterValue("Gain"); 316 | 317 | juce::dsp::AudioBlock block(buffer); 318 | gain.setGainDecibels(currentGain); 319 | gain.process(juce::dsp::ProcessContextReplacing(block)); 320 | } 321 | ``` 322 | 323 | # 总结 324 | 325 | 你可以在这里找到本章中的相关代码: 326 | 327 | [TaroStudio/TaroGain at main · TaroPie1214/TaroStudio (github.com)](https://github.com/TaroPie1214/TaroStudio/tree/main/TaroGain) 328 | 329 | 在这一章中,我们学习了Real-Time效果器的基本写法,比如AudioBuffer,AudioBlock的相关用法等。以及DSP module的使用方式(创建->prepareToPlay中配置spec,processBlock中进行process)等等。 330 | 331 | 接下来我们会继续深入各种常见效果器背后的DSP原理,并将其编写为JUCE效果器~ 332 | --------------------------------------------------------------------------------