系列文章目录
文章目录
一、Vibrato 是什么
“vibrato” 一词指的是,在一个音的音高上小的、准周期性的变化。“vibrato” 一词在不同领域有着不同的翻译,例如在小提琴中,“vibrato” 意旨 “揉弦”,是一种小提琴演奏技巧(参考小提琴手腕揉弦教学 | 英文教学中文字幕 【Penny Teaching】);在歌唱圈中,“vibrato” 指的是颤音(参考 最通俗易懂的颤音教学!你学废了吗?)。维基百科 vibrato 词条中同样也举了唱歌和小提琴的例子,有兴趣可以听听看,提升对 vibrato 感性的认识。
下图是 440hz 正弦波经过 vibrato 音效后的结果,从频谱上能看到音高在周期性的变化。
下面的视频是 Libaa - Vibrato 的效果演示,一个夸张的 Vibrato 会产生相当滑稽的结果:
vibrato
vibrato_video
通过上述的例子,你应该对 vibrato 有了一些基本认识:它一种关于音调变化的音效。
二、Vibrato 原理
2.1 Time-varying delay line
vibrato 实现原理并不复杂,人们通常用一个 delay line 就能够完成 vibrato 算法。请回忆一下之前在【音效处理】Delay/Echo 简介 提到的 Delay 算法,Delay 算法延迟 D D D 个采样,其中 D D D 是一个固定的数字,而在 vibrato 算法中,延迟的时长是一个关于时间 t t t 的函数 D t D_t Dt,也就是说延迟时间是一个不停变化的数字的,我们称为 “Time-varying delay line(可变延迟线)”。
下图为 Vibrato 与 Delay 的块状图,Vibrato 实现中通常用 LFO 来生成
D
t
D_t
Dt,此外 Vibrato 输出只有“湿”信号,没有 feedback 信号;而 Delay 算法中
D
D
D 是固定的,且输出时对 “干”和“湿”信号做了 mix。
2.2 多普勒效应
为什么一个随时间变化的延迟 D t D_t Dt 会造成音调的变化呢?背后的原理其实就是多普勒效应。关于多普勒效应的介绍请参考多普勒效应 - 知乎,本文不再过多赘述。
多普勒效应在生活中非常常见,类比到 Vibrato 音效中,你可以这么想象:有一个喇叭播放着音频,你站在原地,喇叭处于运动状态,它时而靠近你时而远离你,做着周期性的运动。当喇叭靠近你时,距离短,声音从发出到你耳朵的延迟 D D D 小;当喇叭远离你时,距离长,声音从发出到你耳朵的延迟 D D D 大。这意味着 D D D 是一个随时间变化的值,也就是上面提到的 D t D_t Dt。多普勒效应造成了音高的变化。
接下来讨论多普勒效应与 vibrato 之间的关系。首先,多普勒频移公式如下:
ω
l
=
ω
s
1
+
v
l
s
c
1
−
v
s
l
c
(1)
\omega_{l}=\omega_{s} \frac{1+\frac{v_{l s}}{c}}{1-\frac{v_{s l}}{c}} \tag{1}
ωl=ωs1−cvsl1+cvls(1)
其中
ω
s
\omega_{s}
ωs 是声源静止状态下发生音频的频率,
ω
l
\omega_{l}
ωl 是 Listener 接受到的频率,
v
l
s
v_{l s}
vls 表示 Listener 相对于声源方向的传播介质(假设是空气)的速度,
v
s
l
v_{s l}
vsl表示声源相对于 Listener 方向的传播介质的速度,
c
c
c表示声速。在 多普勒效应(一维匀速运动) 有具体的习题,可以参考参考加深理解。
Vibrato 差分方程为:
y
(
t
)
=
x
(
t
−
D
t
)
y(t) = x(t - D_t)
y(t)=x(t−Dt)
其中
D
t
D_t
Dt 为单位为秒的时变延迟,在离散实现中,
D
t
D_t
Dt 不是一个整数,
x
(
t
−
D
t
)
x(t-D_t)
x(t−Dt) 可以采用插值法等技术来近似到任意的精度(参考 Libaa - DelayLine::getInterpolation 方法)。
简单起见,让我们来研究
x
(
t
)
x(t)
x(t) 为复正弦信号的情况,此时
x
(
t
)
x(t)
x(t) 可表示为:
x
(
t
)
=
e
j
ω
s
t
x(t)=e^{j \omega_{s} t}
x(t)=ejωst
此时输出信号为:
y
(
t
)
=
x
(
t
−
D
t
)
=
e
j
ω
s
⋅
(
t
−
D
t
)
y(t)=x\left(t-D_{t}\right)=e^{j \omega_{s} \cdot\left(t-D_{t}\right)}
y(t)=x(t−Dt)=ejωs⋅(t−Dt)
该信号的瞬时相位为:
θ
(
t
)
=
∠
y
(
t
)
=
ω
s
⋅
(
t
−
D
t
)
\theta(t)=\angle y(t)=\omega_{s} \cdot\left(t-D_{t}\right)
θ(t)=∠y(t)=ωs⋅(t−Dt)
然后通过微分得出瞬时频率:
ω
l
=
ω
s
(
1
−
D
˙
t
)
(2)
\omega_{l}=\omega_{s}\left(1-\dot{D}_{t}\right) \tag{2}
ωl=ωs(1−D˙t)(2)
其中
ω
l
\omega_l
ωl表示输出频率,
D
˙
t
≜
Δ
d
D
t
\dot{D}_{t} \triangleq \frac{\Delta}{d} D_{t}
D˙t≜dΔDt 表示延迟
D
t
D_t
Dt 的时间导数。
仔细观察公式(1)和公式(2)发现当
D
˙
t
=
−
v
l
s
c
\dot{D}_{t}=-\frac{v_{l s}}{c}
D˙t=−cvls 时两个公式可以匹配上:
ω
l
=
ω
s
(
1
+
v
l
s
c
)
=
ω
s
1
+
v
l
s
c
1
(3)
\omega_{l}=\omega_{s}\left(1+\frac{v_{l s}}{c}\right)=\omega_{s} \frac{1+\frac{v_{l s}}{c}}{1} \tag{3}
ωl=ωs(1+cvls)=ωs11+cvls(3)
此时,我们发现时变延迟最自然地模拟了由移动的 Listener 引起的多普勒频移。
三、Vibrato C/C++ 实现
3.1 从 LFO 中得到延迟
开篇时提到 Vibrato 中时变延迟由 LFO 生成,但由于我们的 LFO 生成的信号范围总在 [-1, 1] 之间,而延迟时间 D t D_t Dt 应该是一个正数,且在具体的某个范围内,例如 0ms ~ 10ms 之间,因此我们需要对 LFO 的输出进行进一步的加工,得到 D t D_t Dt。
定义 min_delay_ms
为最小延迟,max_delay_ms
为最大延迟,现在我们的问题转换为:将 [-1, 1] 映射到 [min_delay_ms
,max_delay_ms
] 中。这并不复杂,我们假设 min_delay_ms=2
,max_delay_ms=10
,实现的代码为:
float min_delay_ms = 2.0f;
float max_delay_ms = 10.0f;
float delay_half_range = (max_delay_ms - min_delay_ms) / 2.0f; // 4
float middle_point = (min_delay_ms + delay_half_range); // 6
// lfo_output is in range [-1, 1]
float lfo_output = get_lfo_output();
// project [-1, 1] to [2, 10]
float D = lfo_output*delay_half_range + middle_point;
3.2 算法参数
Vibrato 算法有两个可控制的参数:Rate
和 Depth
。
其中 Rate
表示 LFO 的振荡频率,范围通常在 [0.2, 20] 之间,但这个范围只是一个很主观的数字,你可以根据自己需求进行调整。
Depth
范围在 [0, 1] ,它用于控制最大延迟和最小延迟之间的差(你可以想象,sin 信号被压扁了)。当 Depth = 0.5
时,还是以上面的例子为例,延迟范围被压缩到了 [4, 6],伪代码为:
float depath = 0.5f;
// now it is in [-0.5, 0.5]
float lfo_output = get_lfo_output() * 0.5f;
// project [-0.5, 0.5] to [4, 6]
float D = lfo_output*delay_half_range + middle_point;
3.3 C/C++ 实现
Vibrato 可以基于 Libaa - Delay 实现,但请注意 Vibrato 没有 feedback 信号,此外它的输出只有“湿”信号。因此 Delay 算法中 feedback
和 dry
参数都固定为 0。另外我们还引入 LFO 来生成延迟,具体实现请参考 Libaa - Vibrato
四、总结
本文介绍了 Vibrato 音效,它是一种关于音高变化的音效,人们通常使用 Time-varying delay line 来实现 Vibrato,造成其音高变化的背后原理是多普勒效应。Vibrato 算法有 Rate
和 Depth
两个参数,分别控制 LFO 频率和延迟范围,并给出了具体的 C/C++ 实现代码 Libaa - Vibrato