我的Bilibili频道:香芋派Taro
我的个人博客:taropie0224.github.io
我的公众号:香芋派的烘焙坊
我的音频技术交流群:1136403177
我的个人微信:JazzyTaroPie

image-20221213162245532

简介

Amplitude Modulation(调幅)是音频处理中的非常常用的一种方法,它通过将输入信号乘上一个调制信号来获得新的信号。我们通常会使用一个LFO(低频振荡器)作为调制信号,LFO的种类包括正弦波、方波、三角波等等,且它的频率通常不超过20Hz。

Tremolo也是一种非常常见的效果器,它通过上面介绍的Amplitude Modulation来自动化控制输入信号的幅度,实现一种响度忽大忽小的效果。

Github: https://github.com/TaroPie0224/TaroTremolo

原理

我们需要让用户输入两个变量,分别是:

  1. rate,用于控制LFO的频率,通常为0~20
  2. depth,调制的深度,通常为0~100

首先我们需要把depth转化成LFO的幅度,至于为什么是200,之后会提到

1
a  = depth / 200

得到偏置offset

1
offset = 1 - a

定义LFO

1
lfo = a * sin(2 * pi * rate * t) + offset

定义output

1
output = input * lfo

我们可以发现,当depth取最大值100时,a为0.5,此时offset也为0.5,若此时正好震荡到sin为最小值-1的时候,此时lfo的值为0,为调幅的最低点;同理我们也能得到lfo的最大值为1。

这也就是为什么a和offset是这么定义的,这样才能够保证在LFO震荡到最高点时相当于bypass,在LFO震荡到最低点时相当于mute。

当然这也不是强制要求的,你完全可以通过修改你的a和offset来定制LFO得到你想要的tremelo效果,比如不想让它触碰到0等等。

JUCE Code

1
2
3
4
5
6
7
// PluginProcessor.h
...
private:
int sampleCount = 0; // 首先在h文件创建一个float型变量sampleCount
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TaroTremeloAudioProcessor)
};
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
// PluginProcessor.cpp
void TaroTremeloAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();

auto currentRate = apvts.getRawParameterValue("Rate")->load();
auto currentDepth = apvts.getRawParameterValue("Depth")->load();

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

juce::dsp::AudioBlock<float> block {buffer};

// 定义周期
float periodicity = 1 / currentRate;
// 定义采样率
int sampleRate = getSampleRate();
// 开始循环遍历block里所有的sample
for (int sample = 0; sample < block.getNumSamples(); ++sample)
{
// 分离左右声道
auto* channelDataLeft = buffer.getWritePointer(0);
auto* channelDataRight = buffer.getWritePointer(1);

// 把depth转化成LFO的amplitude
float a = currentDepth / 200;
// offset与a相关
float offset = 1 - a;
// 当前时刻,通过已读取的采样数除以采样率得到
float currentTime = static_cast<float>(sampleCount) / static_cast<float>(sampleRate);

// 定义正弦波LFO
auto lfo = a * std::sin(juce::MathConstants<float>::twoPi * currentRate * currentTime) + offset;

// 应用到sample上
channelDataLeft[sample] *= lfo;
channelDataRight[sample] *= lfo;

// 如果当前的sampleCount小于一个周期,那么sampleCount自增
if (currentTime < periodicity)
{
sampleCount++;
}
// 如果等于或大于了,则让sampleCount归零
// 此举是为了限制sampleCount的大小,总不能无限大吧,在一个周期里循环就行了
else
{
sampleCount = 0;
}
}
}