瞬态检测是G719编码的第一个步骤,也是比较容易理解的一步。
瞬态检测原理
瞬态检测是编码的第一部分,主要是用来确定这一帧数据的状态,是变化比较大的,还是比较平稳的。
从文档中可以看出,瞬态检测模块主要分为以下几个部分:
- 48k采样率的信号首先经过高通滤波器,除去不需要的低频部分。
- 然后每5ms计算短时能量值。
- 通过短时能量值计算长时能量。
- 检查短时能量和尝试能量是否满足瞬态检测条件。
高通滤波器
高通滤波器这里使用了一阶IIR滤波器,其公式为:
H
H
P
(
z
)
=
0.7466
(
1
−
z
−
1
)
)
1
−
0.4931
z
−
1
(1)
H_{HP}(z) = \frac{0.7466(1-z^{-1}))}{1-0.4931z^{-1}} \tag{1}
HHP(z)=1−0.4931z−10.7466(1−z−1))(1)
高通滤波器的响应
x
H
P
x_{HP}
xHP是:
x
H
P
(
n
)
=
0.4931
x
H
P
(
n
−
1
)
+
0.7466
x
(
n
)
−
0.7466
x
(
n
−
1
)
(2)
x_{HP}(n) = 0.4931x_{HP}(n-1) + 0.7466x(n) - 0.7466x(n-1) \tag{2}
xHP(n)=0.4931xHP(n−1)+0.7466x(n)−0.7466x(n−1)(2)
其中n = 0,1,2,3,4……,L-1。L是20ms的采样点数,即 960。
短时能量计算
高通滤波器的输出信号
x
H
P
x_{HP}
xHP被分成四等份,每份5ms,也就是 240个点,然后通过公式3计算这一个子帧的短时能量值:
E
(
m
)
=
1
L
/
4
∑
n
=
m
L
/
4
(
m
+
1
)
L
/
4
−
1
)
x
H
P
(
n
)
2
(3)
E(m) = \frac{1}{L/4}\sum_{n=mL/4}^{(m+1)L/4-1)}x_{HP}(n)^{2} \tag{3}
E(m)=L/41n=mL/4∑(m+1)L/4−1)xHP(n)2(3)
其中,m是子帧的个数,即1,2,3,4。
长时能量计算
长时能量计算是根据短时能量计算得到的,长时能量
E
L
T
(
m
)
E_{LT}(m)
ELT(m)的公式为:
E
L
T
(
m
)
=
(
1
−
α
)
E
L
T
(
m
−
1
)
+
α
E
(
m
)
(4)
E_{LT}(m) =(1-\alpha )E_{LT}(m-1) + \alpha E(m)\tag{4}
ELT(m)=(1−α)ELT(m−1)+αE(m)(4)
可以看出长时能量的计算依赖上一次长时能量的计算值和短时能量的加权。这里
α
\alpha
α取值为2.5,$E_{LT}(-1) =E_{LT}(3)
。同时,这里会保存滤波的状态
。同时,这里会保存滤波的状态
。同时,这里会保存滤波的状态H_{HP}(z)
以及
以及
以及E_{LT}(3)$用于后续的计算。
瞬态标记
长时能量和短时能量都计算后,将两者比较,当短时能量值超过某个阈值的时候,认为这是一个瞬态帧,否则,认为这是一个稳态帧,具体的公式如下:
E
(
m
)
≥
ρ
E
L
T
(
m
)
(5)
E(m) \geq \rho E_{LT}(m) \tag{5}
E(m)≥ρELT(m)(5)
其中,能量阈值
ρ
\rho
ρ的取值来自公式:
10
l
o
g
10
(
ρ
)
=
7.8
d
B
10log_{10}(\rho ) = 7.8dB
10log10(ρ)=7.8dB
文档中还提到:一般来说,时频变换应用于40毫秒的帧;因此,一个瞬态会影响两个连续的帧。因此,检测到瞬态时会状态遗留。在某一帧中检测到的瞬态也会在下一帧中触发瞬态。
代码解读
前文提到G719中包含了两份代码,一份是int的实现,一份是浮点数float的 实现。int中会对一些运算做重定义,不方便阅读,这里只介绍float的实现形式,两者差距不大。
代码位置:src/encoder/encode_frame.c
void encode_frame(float *audio,
short *bitstream,
CoderState *c)
{
float wtda_audio[FRAME_LENGTH];
float t_audio[FRAME_LENGTH];
float t_audio_norm[FREQ_LENGTH];
short is_transient;
short bitalloc[NB_SFM];
short i;
short nf_idx;
short bits;
short **pbitstream;
short *tbitstream;
tbitstream = bitstream; //b保存编码输出指针
is_transient = detect_transient(audio, c);// 检测瞬态
// 略
}
进行瞬态检测代码分析之前,首先需要看一下编码过程中的一个重要的结构体参数:
typedef struct
{
float old_wtda[FRAME_LENGTH/2];
float old_hpfilt_in;
float old_hpfilt_out;
float EnergyLT;
short TransientHangOver;
short num_bits;
short num_bits_spectrum_stationary ;
short num_bits_spectrum_transient ;
} CoderState;
- old_wtda表示的是窗数据的保存值,主要是在自适应时频变换过程中使用
- old_hpfilt_in用于保存高通滤波器的上一次输入
- old_hpfilt_out保存高通滤波器的上一次输出值,这两个参数从公式2中可以看出,每次计算都需要进行迭代更新。
- EnergyLT表示长时能量值
- TransientHangOver用于缓存长时能量状态,见瞬态标记章节中的记录。
- num_bits用于表示采样点个数
- num_bits_spectrum_stationary和num_bits_spectrum_stationary后续步骤会使用。
encode_frame函数的代码量在30行左右,将整个G719解码流程包括其中,这一节对应的函数是detect_transient函数,该函数位于encoder/detect_transient.c
文件中。
short detect_transient(float in[], CoderState *c) {
float Energy;
float EnergyLT;
short i, blk;
short IsTransient;
float out_filt[FRAME_LENGTH];
IsTransient = FALSE; // 是稳态
/* High pass filter */
hp_filter(in, out_filt, &(c->old_hpfilt_in), &(c->old_hpfilt_out)); //先通过高通滤波器
/* Long term energy */
EnergyLT = c->EnergyLT; //
/* Compute block Energy */
for(blk = 0; blk < 4; blk++) { //分成4次,每次5ms
Energy = EPSILON; // 初始化为 0
for(i = 0; i < FRAME_LENGTH / 4; i++) { //高通滤波的输出
Energy += out_filt[i + blk*(FRAME_LENGTH / 4)] * out_filt[i + blk*(FRAME_LENGTH / 4)]; //短时能量
}
if(Energy > 6.0f * EnergyLT) {//10log10 p = 7.8db p = 6
IsTransient = TRUE; //是暂态的
}
EnergyLT = 0.75f*EnergyLT + 0.25f*Energy; //长时能量
}
c->EnergyLT = EnergyLT;//缓存长时能量
if(IsTransient) {
/* Set Hangover 设置缓存 */
c->TransientHangOver = TRUE;
} else {
if( c->TransientHangOver ) { //由于 G.719 的时频变换涉及前后两个帧,共 40ms,暂态检测将影响到前后连续的帧,因此引入暂态检测遗留标记,这样当前一帧被检测为暂态时,则下一帧也将按暂态方式处理。
c->TransientHangOver = FALSE;
IsTransient = TRUE;
}
}
return IsTransient;
}
detect_transient代码的第七行,默认定义了变量IsTransient
表示稳态数据,根据文章开始的流程图,首先将数据进行滤波,滤波调用的函数是hp_filter
:
void hp_filter(float x[], float y[], float *oldy, float *oldx) {//Yk = A*Yk−1+A*(Xk − Xk−1)
short i;
y[0] = 0.4931f * *oldy + 0.7466f*(x[0] - *oldx);
for(i = 1; i < FRAME_LENGTH; i++) {
y[i] = 0.4931f*y[i-1] + 0.7466f*(x[i] - x[i-1]);
}
*oldx = x[FRAME_LENGTH-1];
*oldy = y[FRAME_LENGTH-1];
}
- hp_filter中,x是输入,y是输出,oldy和oldx分别是上一次滤波的输入和输出。
- 第三行首先对输出的第一个元素使用上一次的数据进行计算。
- 对于后续元素,直接在for循环中计算959次即可。使用的公式是公式2,参数也是如此。
- 最后在第7行和第8行最后一个数据重新缓存到oldy和oldx,根据detect_transient的入参,这两个参数保存至c->old_hpfilt_in和c->old_hpfilt_out中。
再回头看detect_transient:第九行会保存上一次的长时能量值,为公式4准备。
11行将20ms的数据分成4份计算,每5ms计算一次短时能量,根据公式3,见代码行13-15.
短时能量计算后,在16-18行判断,如果大于阈值,表示是暂态信号,更新IsTransient
的值,见公式5。
19行会根据最新的短时能量更新长时能量的值,对照公式4,更新完后会更新编码状态数据结构的长时能量缓存值。
最后一个参数就是瞬态标记的更新,如果判断出是稳态信号,但是有记录c->TransientHangOver是TRUE,即上一次帧是暂态帧,那么这一帧也标记为瞬态,并重置数据。