基于Kaidi谱特征的语音重构

第一次是拿C写的,提谱特征,过增强网络,之后取噪声相位,重构音频,弄了一个星期,后来发现,其实可以不必这么麻烦的。用kaldi得到的增强特征,做一个逆的CMVN,之后拿Python处理一下特征还原就行了。

总结一下,语音重构主要是还原频域特征到时域上,使用原始音频的相位信息,流程如下

  1. 获取一帧的谱特征
  2. 获取原始音频中对应帧的相位
    a. 分帧
    b. Remove DC
    c. 预加重
    d. 加窗
    e. RFFT,获取相位,返回
  3. 对谱特征,取exp,开方得到幅度谱,这里只使用[1: 257]的值,不使用能量
  4. 幅度谱和相位点乘,RFFT,取前400维得到一帧数据
  5. 加窗
  6. 进行OverlapAdd, 实际上就是帧移相加
  7. 所有帧处理完之后,加一个低通滤波器
  8. 将采样值的范围恢复到原始音频的范围内

代码如下,理清特征处理过程思路就很清晰了。

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
#!/usr/bin/env python

"""transform spectrogram to waveform"""

import sys
import wave
import numpy as np

import kaldi_io
import wave_io


if len(sys.argv) != 4:
print "format error: %s [spectrum] [origin-wave] [reconst-wave]" % sys.argv[0]
sys.exit(1)

WAVE_WARPPER = wave_io.WaveWrapper(sys.argv[2])
WAVE_RECONST = wave.open(sys.argv[3], "wb")

WND_SIZE = WAVE_WARPPER.get_wnd_size()
WND_RATE = WAVE_WARPPER.get_wnd_rate()

REAL_IFFT = np.fft.irfft

HAM_WND = np.hamming(WND_SIZE)

with open(sys.argv[1], "rb") as ark:
SPECT_ENHANCE = kaldi_io.next_mat_ark(ark)
SPECT_ROWS, SPECT_COLS = SPECT_ENHANCE.shape
assert WAVE_WARPPER.get_frames_num() == SPECT_ROWS
INDEX = 0
SPECT = np.zeros(SPECT_COLS)
RECONST_POOL = np.zeros((SPECT_ROWS - 1) * WND_RATE + WND_SIZE)
for phase in WAVE_WARPPER.next_frame_phase():
# exclude energy
SPECT[1: ] = np.sqrt(np.exp(SPECT_ENHANCE[INDEX][1: ]))
RECONST_POOL[INDEX * WND_RATE: INDEX * WND_RATE + WND_SIZE] += \
REAL_IFFT(SPECT * phase)[: WND_SIZE] * HAM_WND
INDEX += 1
for x in range(1, RECONST_POOL.size):
RECONST_POOL[x] += 0.97 * RECONST_POOL[x - 1]
RECONST_POOL = RECONST_POOL / np.max(RECONST_POOL) * WAVE_WARPPER.get_upper_bound()

WAVE_RECONST.setnchannels(1)
WAVE_RECONST.setnframes(RECONST_POOL.size)
WAVE_RECONST.setsampwidth(2)
WAVE_RECONST.setframerate(WAVE_WARPPER.get_sample_rate())
WAVE_RECONST.writeframes(np.array(RECONST_POOL, dtype=np.int16).tostring())
WAVE_RECONST.close()

当年python画风好奇怪,顺便补充一下谱特征的正向处理过程,我拿python写的结果和kaldi做了一下对比,在不加随机高斯量的时候,误差还是很小的。

kaldi中默认的普特征提取流程如下

  1. 分帧【加一个随机高斯量,可以通过options去掉,默认为真】
  2. Remove DC, 也就是减去帧的均值,移除直流分量
  3. 计算原始能量,放在第一维上
  4. 预加重
  5. 加窗,这里加的是指定类型的窗
  6. RFFT, 取[1, 256]区间,注意,这里是能量log谱,不是幅度log谱
  7. 返回一帧的谱特征,继续

Demo代码如下

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
#!/usr/bin/env python

"""compute spectrogram according to kaldi"""

import sys
import wave
import math
import numpy as np


if len(sys.argv) != 2:
print "format error: %s [wave-in]" % sys.argv[0]
sys.exit(1)

SRC_WAVE = wave.open(sys.argv[1], "rb")
SRC_SAMPLE_RATE, TOT_SAMPLE = SRC_WAVE.getparams()[2: 4]

WND_SIZE = int(SRC_SAMPLE_RATE * 0.001 * 25)
WND_OFFSET = int(SRC_SAMPLE_RATE * 0.001 * 10)
WAVE_DATA = np.fromstring(SRC_WAVE.readframes(TOT_SAMPLE), np.int16)

FRAME_NUM = (WAVE_DATA.size - WND_SIZE) / WND_OFFSET + 1
# FRAME_VEC = np.zeros(WND_SIZE)

SPECT_LEN = 257
SPECT_VEC = np.zeros(SPECT_LEN)

HAMMING = np.hamming(WND_SIZE)

print FRAME_NUM

for index in range(FRAME_NUM):
BASE_PNT = index * WND_OFFSET
# get frame
FRAME_VEC = np.array(WAVE_DATA[BASE_PNT: BASE_PNT + WND_SIZE], dtype=np.float)
# dither...
# remove dc mean
FRAME_VEC -= (np.sum(FRAME_VEC) / WND_SIZE)
# calculate log energy
energy = math.log(np.sum(FRAME_VEC ** 2))
# preemphasize
FRAME_VEC[1: ] -= 0.97 * FRAME_VEC[: -1]
FRAME_VEC[0] -= 0.97 * FRAME_VEC[0]

# buffer
DFT_VALUE = np.zeros((SPECT_LEN - 1) * 2)
# hamming
DFT_VALUE[: WND_SIZE] = FRAME_VEC * HAMMING
# power log
SPECT_VEC[0] = energy
SPECT_VEC[1: ] = np.log(np.abs(np.fft.rfft(DFT_VALUE)[1: ]) ** 2)
# print SPECT_VEC
# print np.log(np.abs(np.fft.rfft(DFT_VALUE)) ** 2)