语音数据增强及python实现

目录

博客作者:凌逆战

博客地址:https://www.cnblogs.com/LXP-Never/p/13404523.html


  音频时域波形具有以下特征:音调,响度,质量。我们在进行数据增强时,最好只做一些小改动,使得增强数据和源数据存在较小差异即可,切记不能改变原有数据的结构,不然将产生“脏数据”,通过对音频数据进行数据增强,能有助于我们的模型避免过度拟合并变得更加通用。

  我发现对声波的以下改变是有用的:Noise addition(增加噪音)、Add reverb增加混响)Time shifting(时移)Pitch shifting(改变音调)Time stretching(时间拉伸)

本章需要使用的 python 库:

  • matplotlib:绘制图像
  • librosa:音频数据处理
  • numpy:矩阵数据处理

常见的失真有:

  1. 加性声学噪声:加性噪声与期望信号不相干,平稳加性噪声 (背景环境声音、嗡嗡声、功放噪音),非平稳加性噪声 (媒体干扰、非期望语音干扰和一些电子干扰)
  2. 声学混响:多径反射引起的叠加效应(与期望信号相关)
  3. 卷积信道效应:导致不均匀或带宽限制响应,为了去除信道脉冲响应,做信道均衡时对通信信道没有有效建模
  4. 非线性失真:信号输入时不适当的增益,常出现与幅度限制、麦克风功放等
  5. 加性宽带电子噪声
  6. 电器干扰
  7. 编码失真:比如压缩编码
  8. 录音仪器引起的失真:麦克风频率响应不足

  使用先画出原始语音数据的语谱图和波形图

import librosa
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示符号
fs = 16000

wav_data, _ = librosa.load("./p225_001.wav", sr=fs, mono=True)

# ########### 画图
plt.subplot(2, 2, 1)
plt.title(
"语谱图", fontsize=15)
plt.specgram(wav_data, Fs
=16000, scale_by_freq=True, sides='default', cmap="jet")
plt.xlabel(
'秒 /s', fontsize=15)
plt.ylabel(
'频率 /Hz', fontsize=15)

plt.subplot(2, 2, 2)
plt.title(
"波形图", fontsize=15)
time
= np.arange(0, len(wav_data)) * (1.0 / fs)
plt.plot(time, wav_data)
plt.xlabel(
'秒 /s', fontsize=15)
plt.ylabel(
'振幅', fontsize=15)

plt.tight_layout()
plt.show()

时域增强

噪声增强

  通过信噪比的公式推导出噪声的增益系数 k。

SNR=10∗log10(S2(kN)2)⇒k=S2N2∗10SNR10

def snr_aug_changeNoise(clean, noise, snr):
    """ 保持语音不变,改变噪声的幅度
    snr = 10 * log10(signal_power / k*noise_power) """
    p_clean = np.mean(clean ** 2)  # 纯净语音功率
    p_noise = np.mean(noise ** 2)  # 噪声功率
    noise_scale = np.sqrt(p_clean / (p_noise * 10 ** (snr / 10) + EPS))
    noisy = clean + noise_scale * noise
    return noisy, noise_scale

def snr_aug_changeClean(clean, noise, snr):
""" 保持噪声不变,改变语音的幅度
snr = 10 * log10(k*signal_power/ noise_power)
"""
clean_power
= np.mean(clean ** 2)
noise_power
= np.mean(noise ** 2)
clean_scale
= np.sqrt(noise_power * 10 ** (snr / 10) / (clean_power + 1e-8))
noisy
= clean * clean_scale + noise

</span><span style="color: rgba(0, 0, 255, 1)">return</span> noisy, clean_scale</pre>

音量增强

  语音音量的单位为 dB,音量增益可以基于平均音量或者最大瞬时音量,下面公式是基于平均音量推得 dB 增益:

dB=10∗log10(kS)2⇒k=10dB10S2

def volumeAument1(wav, dB):
    """
    :param wav: 语音
    :param dB: 音量
    :return: 返回以指定 dB 增益后的语音
    """
    power = np.mean(wav ** 2)  # 平均功率
    scalar = np.sqrt(10 ** (dB / 10) / (power + np.finfo(np.float32).eps))
    wav *= scalar
    return wav, scalar

dB=20∗log10kS⇒k=10db20A

def volumeAument2(wav, dB):
    """
    :param wav: 语音
    :param dB: 音量
    :return: 返回以指定 dB 增益后的语音
    """
    rmswav = (wav ** 2).mean() ** 0.5
    scalar = 10 ** (dB / 20) / (rmswav + np.finfo(np.float32).eps)
    wav = wav * scalar
    return wav, scalar

其实这两个函数都可以,都可以达到目的,本质上都一样。

# -*- coding:utf-8 -*-
# Author: 凌逆战 | Never
# Date: 2023/3/17
"""
音量增强
"""
import numpy as np
import librosa

EPS = np.finfo(float).eps

def mean_dbfs(sample_data):
rms
= np.sqrt(np.mean(np.square(sample_data, dtype=np.float64)))
dbfs
= 20.0 * np.log10(max(1e-16, rms))
return dbfs

def volumeAument1(wav, dB):
"""
:param wav: 语音
:param dB: 音量
:return: 返回以指定 dB 增益后的语音
"""
power
= np.mean(wav ** 2) # 平均功率
scalar = np.sqrt(10 ** (dB / 10) / (power + np.finfo(np.float32).eps))
wav
*= scalar
return wav, scalar

def volumeAument2(wav, dB):
"""
:param wav: 语音
:param dB: 音量
:return: 返回以指定 dB 增益后的语音
"""
rmswav
= (wav ** 2).mean() ** 0.5
scalar
= 10 ** (dB / 20) / (rmswav + np.finfo(np.float32).eps)
wav
= wav * scalar
return wav, scalar

sr = 16000
wav
= librosa.load("./wavdata/TIMIT.WAV", sr=sr)[0] # (46797,)
print(wav.shape)
wav, scalar
= volumeAument1(wav, dB=15)
print(mean_dbfs(wav)) # 18.0103004778581

wav, scalar
= volumeAument2(wav, 15)
print(mean_dbfs(wav)) # 18.010299731550788

View Code

另外,如果我们是想对带噪语音进行音量增益,为了不破坏带噪语音的信噪比,还需要对纯净语音语音进行相同的增益

noisy, noisy_scalar = dB_gain(noisy, 20)    # 得到增益后的带噪语音和增益系数
clean *= noisy_scalar    # 为了控制 snr 不变,clean 需要进行相同的增益

混响增强

  我这里使用的是 Image Source Method(镜像源方法)来实现语音加混响,我想用两种方法来给大家实现,第一种是直接调用 python 库—— Pyroomacoustics来实现音频加混响,第二种就是按照公式推导一步一步来实现,两种效果一样,想看细节的可以参考第二种方法,只想开始实现效果的可以只看第一种方法:

方法一: Pyroomacoustics实现音频加混响

  首先需要安装 Pyroomacoustics,这个库非常强大,感兴趣也可以多看看其他 API 接口

pip install  Pyroomacoustics

步骤:

  1. 创建房间(定义房间大小、所需的混响时间、墙面材料、允许的最大反射次数、)
  2. 在房间内创建信号源
  3. 在房间内放置麦克风
  4. 创建房间冲击响应
  5. 模拟声音传播
# Author: 凌逆战
# -*- coding:utf-8 -*-
import pyroomacoustics as pra
import numpy as np
import matplotlib.pyplot as plt
import librosa

# 1、创建房间
#
所需的混响时间和房间的尺寸
rt60_tgt = 0.5 # 所需的混响时间,秒
room_dim = [9, 7.5, 3.5] # 我们定义了一个 9m x 7.5m x 3.5m 的房间,米

# 我们可以使用 Sabine’s 公式来计算壁面能量吸收和达到预期混响时间所需的 ISM 的最大阶数 (RT60,即 RIR 衰减 60 分贝所需的时间)
e_absorption, max_order = pra.inverse_sabine(rt60_tgt, room_dim) # 返回 墙壁吸收的能量 和 允许的反射次数
#
我们还可以自定义 墙壁材料 和 最大反射次数
#
m = pra.Material(energy_absorption="hard_surface") # 定义 墙的材料,我们还可以定义不同墙面的的材料
#
max_order = 3

room
= pra.ShoeBox(room_dim, fs=16000, materials=pra.Material(e_absorption), max_order=max_order)

# 在房间内创建一个位于 [2.5,3.73,1.76] 的源,从 0.3 秒开始向仿真中发出 wav 文件的内容
audio, _ = librosa.load("speech.wav",sr=16000) # 导入一个单通道语音作为源信号 source signal
room.add_source([2.5, 3.73, 1.76], signal=audio, delay=0.3)

# 3、在房间放置麦克风
#
定义麦克风的位置:(ndim, nmics) 即每个列包含一个麦克风的坐标
#
在这里我们创建一个带有两个麦克风的数组,
#
分别位于 [6.3,4.87,1.2] 和[6.3,4.93,1.2]。
mic_locs = np.c_[
[
6.3, 4.87, 1.2], # mic 1
[6.3, 4.93, 1.2], # mic 2
]

room.add_microphone_array(mic_locs) # 最后将麦克风阵列放在房间里

# 4、创建房间冲击响应(Room Impulse Response)
room.compute_rir()

# 5、模拟声音传播,每个源的信号将与相应的房间脉冲响应进行卷积。卷积的输出将在麦克风上求和。
room.simulate()

# 保存所有的信号到 wav 文件
room.mic_array.to_wav("./guitar_16k_reverb_ISM.wav", norm=True, bitdepth=np.float32,)

# 测量混响时间
rt60 = room.measure_rt60()
print("The desired RT60 was {}".format(rt60_tgt))
print("The measured RT60 is {}".format(rt60[1, 0]))

plt.figure()
# 绘制其中一个 RIR. both can also be plotted using room.plot_rir()
rir_1_0 = room.rir[1][0] # 画出 mic 1 和 source 0 之间的 RIR
plt.subplot(2, 1, 1)
plt.plot(np.arange(len(rir_1_0))
/ room.fs, rir_1_0)
plt.title(
"The RIR from source 0 to mic 1")
plt.xlabel(
"Time [s]")

# 绘制 microphone 1 处接收到的信号
plt.subplot(2, 1, 2)
plt.plot(np.arange(len(room.mic_array.signals[
1, :])) / room.fs, room.mic_array.signals[1, :])
plt.title(
"Microphone 1 signal")
plt.xlabel(
"Time [s]")

plt.tight_layout()
plt.show()

room = pra.ShoeBox(
    room_dim,
    fs=16000,
    materials=pra.Material(e_absorption),
    max_order=3,
    ray_tracing=True,
    air_absorption=True,
)

# 激活射线追踪
room.set_ray_tracing()

混合 ISM/ 射线跟踪房间模拟器
room.simulate(reference_mic=0, snr=10)      # 控制信噪比
控制信噪比

方法二:Image Source Method 算法讲解

  从这里要讲算法和原理了,

代码参考:matlab 版本:RIR-Generator,python 版本:rir-generator

镜像源法简介:

 

  将反射面等效为一个虚像,或者说镜像。比如说,在一个开放空间里有一面平整墙面,那么一个声源可以等效为 2 两个声源;一个开放空间里有两面垂直的平整墙面,那么一个声源可以等效为 4 个;同理三面的话是 8 个。原理上就是这样,但是封闭的三维空间里情况有那么点复杂,

  一般来说,家里的空房间可以一定程度上近似为矩形盒子,假设房间尺寸为:

L=[xr,yr,zr]

元素大小分别代表长宽高,而声源的三维坐标为

S=[xs,ys,zs]

麦克风的三维坐标为

M=[xm,ym,zm]

镜像声源 (i,j,k) 到麦克风距离在三个坐标轴上的位置为

xi=(−1)ixs+[i+(1−(−1)i)/2]xr−xm

yj=(−1)jys+[j+(1−(−1)j)/2]yr−ym

zk=(−1)kzs+[k+(1−(−1)k)/2]zr−zm

那么声源 (i,j,k) 距离麦克风的距离为

dijk=(xi2+yj2+zk2)

相对于直达声的到达延迟时间为

τijk=(dijk−r)/c

其中 c 为声速,r 为声源到麦克风的直线距离。那么,混响效果等效为不同延迟的信号的叠加,即混响效果可以表示为一个 FIR 滤波器与信号源卷积的形式,此滤波器可写为如下形式

h(t)=∑i∑j∑k[Aijkδ(t−τijk)]

滤波器的抽头系数与镜面的反射系数与距离相关,如果每个面的反射系数不同则形式略复杂。详细代码还是要看RIR-Generator,我这里只做抛转引玉,写一个最简单的。

模拟镜像源:

房间尺寸 (m):4 X 4 X 3

声源坐标 (m):2 X 2 X 0

麦克风坐标 (m):2 X 2 X 1.5

混响时间 (s):0.2

RIR 长度:512

clc;clear;
c = 340;                    % 声速 (m/s)
fs = 16000;                 % Sample frequency (samples/s)
r = [2 2 1.5];              % 麦克风位置 [x y z] (m)
s = [2 2 0];              % 扬声器位置 [x y z] (m)
L = [4 4 3];                % 房间大小 [x y z] (m)
beta = 0.2;                 % 混响时间 (s)
n = 512;                   % RIR 长度

h = rir_generator(c, fs, r, s, L, beta, n);
disp(size(h))
% (1,4096)

[speech, fs] = audioread("./test_wav/p225_001.wav");
disp(size(speech));
% (46797,1)

y = conv(speech', h);
disp(length(y))

% 开始画图
figure(
'color','w'); % 背景色设置成白色
subplot(
3,1,1)
plot(h)
title(
"房间冲击响应 RIR","FontSize",14)

subplot(3,2,3)
plot(speech)
title(
"原语音波形","FontSize",14)

subplot(3,2,4)
plot(y)
title(
"加混响语音波形","FontSize",14)

subplot(3,2,5)
specgram(speech,
512,fs,512,256);
title(
"原语音频谱","FontSize",14)

subplot(3,2,6)
specgram(y,
512,fs,512,256);
title(
"加混响语音频谱","FontSize",14)

audiowrite("./test_wav/matlab_p225_001_reverber.wav",y,fs)

Image Source 方法

gpuRIR:使用图像源方法 (ISM) 和 GPU 加速进行房间脉冲响应 (RIR) 模拟

一种专用于机器学习应用中音频数据增强的随机房间脉冲响应生成方法。与图像源或光线追踪等几何方法相反,这种技术不需要预先定义房间几何形状、吸收系数或麦克风和源位置,并且仅依赖于房间的声学参数。该方法直观、易于实施,并允许生成非常复杂的外壳的 RIR

基于神经网络的快速房间脉冲响应生成器

基于生成对抗网络 (GAN) 的房间脉冲响应发生器 (IR-GAN)

一种提高远场语音识别合成房间脉冲响应质量的方法

在房间脉冲响应 (RIR) 生成中加入以下因素的影响:空气吸收、真实材料的表面和频率相关系数以及随机光线追踪

方法三:利用 RIR 生成混响

def add_pyreverb(wav, rir):
    reverb_wav = signal.fftconvolve(wav, rir, mode="full")
    reverb_wav = reverb_wav[0: wav.shape[0]]  # 使 reverb_wav 和 wav 具有相同的长度
    return reverb_wav

指定 SER 生成远端语音

  SER 的公式为

SER=10log10⁡E{s2(n)}E{d2(n)}

其中 E 是统计 期望操作,s(n) 是近端语音,d(n) 是远端回声,

  由于我们需要根据指定的 SER 求混响信号,并且近端语音和远端混响都是已知的,我们只需要求得一个系数,来调整回声信号的能量大小,与远端混响相乘即可得我们想要的混响语音,即调整后的回声信号为 kd(n)

根据以上公式,可以推导出 k 的值

k=E{s2(n)}E{d2(n)}∗10SER10

最终 kd(n) 即我们所求的指定 SER 的混响。

def add_echo_ser(near_speech, far_echo, SER):
    """根据指定的 SER 求回声
    :param near_speech: 近端语音
    :param far_echo: 远端回声
    :param SER: 指定的 SER
    :return: 指定 SER 的回声
    """
    p_near_speech = np.mean(near_speech ** 2)  # 近端语音功率
    p_far_echo = np.mean(far_echo ** 2)  # 远端回声功率

    k = np.sqrt(p_near_speech / (10 ** (SER / 10)) / p_far_echo)
</span><span style="color: rgba(0, 0, 255, 1)">return</span> k * far_echo</pre>

 

波形位移

  语音波形移动使用 numpy.roll 函数向右移动 shift 距离

numpy.roll(a, shift, axis=None)

参数

  • a:数组
  • shift:滚动的长度
  • axis:滚动的维度。0 为垂直滚动,1 为水平滚动,参数为 None 时,会先将数组扁平化,进行滚动操作后,恢复原始形状
x = np.arange(10)
# array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

print(np.roll(x, 2))
# array([8, 9, 0, 1, 2, 3, 4, 5, 6, 7])

波形位移函数:

def time_shift(x, shift):
    # shift:移动的长度
    return np.roll(x, int(shift))

Augmentation = time_shift(wav_data, shift=fs//2)

谐波失真

参考自:soundpy

def harmonic_distortion(wav):
    wav = 2 * np.pi * wav
    count = 0
    while count < 5:
        wav = np.sin(wav)
        count += 1
    return wav

重采样数据增强

  重采样后语音数据会丢失 重采样采样率到源采样值之间的频谱信息。

def augment_resample(wav, sr):
    resample_sr = np.random.uniform(sr)     # 从一个均匀分布中随机采样
    print("target_sr", resample_sr)
    resample = librosa.resample(wav, orig_sr=sr, target_sr=resample_sr)
    resample = librosa.resample(resample, orig_sr=resample_sr, target_sr=sr)
    return resample

编解码数据增强

  这个我有空再来补全,参考 DeepSpeech,先 numpu to pcm,在转成 opus。

频域增强

音高增强 (Pitch Shifting)

  在频率轴上缩放频谱图,从而改变音高。音高修正只改变音高而不影响音速,我发现 -5 到 5 之间的步数更合适

# sr: 音频采样率
# n_steps: 要移动多少步
# bins_per_octave: 每个八度音阶 (半音) 多少步

# 上移大三度(如果 bins_per_octave 为 12,则 4 步)
augment = librosa.effects.pitch_shift(wav_data, sr=sr, n_steps=4, bins_per_octave=12)
# 向下移动一个三全音(如果 bins_per_octave 是 12,则为六步)
augment = librosa.effects.pitch_shift(wav_data, sr=sr, n_steps=-6, bins_per_octave=12)
# 上移 3 个四分音符
augment = librosa.effects.pitch_shift(wav_data, sr=sr, n_steps=3, bins_per_octave=24)

速度增强 (Tempo)

在时间轴上缩放频谱图,从而改变播放速度。

变速不变调

方法一:ffmpeg

  在变速之前我们需要安装 pip install ffmpeg  

from ffmpeg import audio

# 加快 2 倍速度
audio.a_speed("./sample/p225_001.wav",speed=2,out_file="./sample/p225_001_2.wav")

# 放慢 2 倍速度
audio.a_speed("./sample/p225_001.wav",speed=0.5,out_file="./sample/p225_001_0.5.wav")

ffmpeg 是基于 fmpeg 开发的,Python 的这个库不能加载太大的文件,但是原生的 fmpeg。或者我们可以直接使用原生的 ffmpeg 工具包

我们可以看到变速前后的波形图和语谱图没变,但是他们的时间维度却减少了一半。

方法二:SoundTorch

  SoundTouch 是一个开源音频处理库,用于更改音频流或音频文件的速度、音高和播放速率。该库还支持估计音轨的稳定每分钟节拍率。

命令实例见:https://www.surina.net/soundtouch/soundstretch.html

速度增加 100%

soundstretch input.wav output.wav -tempo=100

速度降低 50%

soundstretch input.wav output.wav -tempo=-50

变速变调

方法一:SOX

  需要在 linux 上运行,具体参考https://github.com/rabitt/pysox

import soundfile
import sox

sr = 16000

tfm = sox.Transformer() # create transformer
tfm.speed(2) # 变速 2 倍

# 创建输出文件
#
tfm.build_file("./sample/p225_001.wav", "./sample/pysox_2x.wav")

# 内存中以 numpy 数组的形式获取输出
array_out = tfm.build_array(input_filepath="./sample/p225_001.wav")
soundfile.write(
"./sample/pysox_2x.wav",data=array_out,samplerate=sr)

或者我们直接使用原生的sox 工具包

$ sox input.wav output.wav speed 1.3 #速度变为原来的 1.3 倍
$ sox input.wav output.wav speed 0.8 #速度变为原来的 0.8 倍

方法二:librosa

  按固定速率对音频系列进行时间拉伸。

def time_stretch(x, rate):
    # rate:拉伸的尺寸,
    # rate > 1 加快速度
    # rate < 1 放慢速度
    return librosa.effects.time_stretch(x, rate)

Augmentation = time_stretch(wav_data, rate=2)

  我们来观察语谱图和波形图,发现形状变了,并且变速后的语音波形振幅降低了,为什么呢?难道变速还会减少语音的音量?求解答


  SpecAugment 通过在时间方向上通过在时间方向上扭曲来增强,并屏蔽(多个)连续时间步长(垂直掩模)和 mel 频率通道(水平掩模)的块

  1. 帮助网络在时间方向上的变形、频率信息的部分丢失和输入的小段语音的部分丢失方面具有鲁棒性
  2. 防止网络过度拟合

SpecAugment 中有三种增强策略:

  • 时间扭曲(Time Warping):在时间轴上随机扭曲频谱图。与速度扰动不同,这种方法不会增加或减少持续时间,而是在局部压缩和拉伸频谱图。
  • 频率掩蔽(Frequency Mask):频谱图的 连续频率 bin 被随机掩蔽
  • 时间掩蔽(Time Mask):频谱图的 连续时间帧被掩蔽

paperwithcode:SpecAugment 几乎所有的代码都列出来了

扭曲增强 (Warp)

  将非线性图像扭曲应用于频谱图。这是通过沿时间和频率轴随机移动均匀分布的扭曲点网格来实现的。代码修改自:DeepSpeech

def tf_pick_value_from_range(value, r, clock=None, double_precision=False):
    clock = (tf.random.stateless_uniform([], seed=(-1, 1), dtype=tf.float64) if clock is None
             else tf.maximum(tf.constant(0.0, dtype=tf.float64), tf.minimum(tf.constant(1.0, dtype=tf.float64), clock)))
    value = tf.random.stateless_uniform([],
                                        minval=value - r,
                                        maxval=value + r,
                                        seed=(clock * tf.int32.min, clock * tf.int32.max),
                                        dtype=tf.float64)
    if isinstance(value, int):
        return tf.cast(tf.math.round(value), tf.int64 if double_precision else tf.int32)
    return tf.cast(value, tf.float64 if double_precision else tf.float32)

def Warp(spectrogram, num_t=1, num_f=1, warp_t=0.1, warp_f=0.0, r=0, clock=0.0):
"""
:param spectrogram: tensor (batch size,t,f)
:param num_t:
:param num_f:
:param warp_t:
:param warp_f:
:param r: 波动范围
:param clock:
:return:
"""
size_t, size_f
= spectrogram.shape

seed </span>= (clock * tf.int32.min, clock *<span style="color: rgba(0, 0, 0, 1)"> tf.int32.max)

num_t </span>= tf_pick_value_from_range(num_t, r, clock=<span style="color: rgba(0, 0, 0, 1)">clock)
num_f </span>= tf_pick_value_from_range(num_f, r, clock=<span style="color: rgba(0, 0, 0, 1)">clock)

</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> get_flows(n, size, warp, r):
    warp </span>= tf_pick_value_from_range(warp, range, clock=<span style="color: rgba(0, 0, 0, 1)">clock)
    warp </span>= warp * tf.cast(size, dtype=tf.float32) / tf.cast(2 * (n + 1), dtype=<span style="color: rgba(0, 0, 0, 1)">tf.float32)
    f </span>= tf.random.stateless_normal([num_t, num_f], seed, mean=0.0, stddev=warp, dtype=<span style="color: rgba(0, 0, 0, 1)">tf.float32)
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> tf.pad(f, tf.constant([[1, 1], [1, 1]]), <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">CONSTANT</span><span style="color: rgba(128, 0, 0, 1)">'</span>)  <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> zero flow at all edges</span>
flows = tf.stack([get_flows(num_t, size_t, warp_t, r), get_flows(num_f, size_f, warp_f, r)], axis=2) flows = tf.image.resize_bicubic(tf.expand_dims(flows, 0), [size_t, size_f]) spectrogram_aug = tf.contrib.image.dense_image_warp(tf.expand_dims(spectrogram, -1), flows) spectrogram_aug = tf.reshape(spectrogram_aug, shape=(1, -1, size_f)) return spectrogram_aug

频率掩膜 (Frequency Mask)

在幅度谱随机的将频点置零。有关更多详细信息,请参阅 SpecAugment 论文:SpecAugment: A Simple Data Augmentation Method for Automatic Speech Recognition

def FreqMask(mag, num_mask=1, mask_percentage=0.01):
    """
    :param mag: (F,T)
    :param num_freq_mask: mask 的数量
    :param mask_percentage: mask 的百分比 0.001~0.015
    """
    F = mag.shape[0]  # 频点数
    mask_width = int(mask_percentage * F)  # mask 的宽度
    for i in range(num_mask):
        mask_start = np.random.randint(low=0, high=F - mask_width)  # mask 的 index
        mag[mask_start: mask_start + mask_width:] = 0   # 掩码 F 维度
    return mag

时间掩码 (Time Mask)

在频谱上在时间维度将帧置零。代码修改自:DeepSpeech

def TimeMask(mag, num_mask=1, mask_percentage=0.01):
    """
    :param mag: (F,T)
    :param num_freq_mask: mask 的数量
    :param mask_percentage: mask 的百分比 0.001~0.015
    """
    T = mag.shape[1]  # 频点数
    mask_width = int(mask_percentage * T)  # mask 的宽度
    for i in range(num_mask):
        mask_start = np.random.randint(low=0, high=T - mask_width)
        mag[:, mask_start:mask_start + mask_width] = 0  # 掩码 T 维度
    return mag

频谱交换 (SpecSwap)

X. Song, Z. Wu, Y. Huang, D. Su, and H. Meng, "SpecSwap: A Simple Data Augmentation Method for End-to-End Speech Recognition", in INTERSPEECH, 2020.

频谱交换提供两个增强策略:

  • 频率交换:随机交换频谱图的两个频率块
  • 时间交换:在时间轴上随机交换频谱图的两个帧块

SpecSwap 增强策略在 E2E ASR 模型中也运行良好,但缺乏与 SpecAugment 的比较。

多领域增强

drop 增强

  将目标数据表示的随机数据点归零。代码修改自:DeepSpeech

def tf_pick_value_from_range(value, r, clock=None, double_precision=False):
    clock = (tf.random.stateless_uniform([], seed=(-1, 1), dtype=tf.float64) if clock is None
             else tf.maximum(tf.constant(0.0, dtype=tf.float64), tf.minimum(tf.constant(1.0, dtype=tf.float64), clock)))
    value = tf.random.stateless_uniform([],
                                        minval=value - r,
                                        maxval=value + r,
                                        seed=(clock * tf.int32.min, clock * tf.int32.max),
                                        dtype=tf.float64)
    if isinstance(value, int):
        return tf.cast(tf.math.round(value), tf.int64 if double_precision else tf.int32)
    return tf.cast(value, tf.float64 if double_precision else tf.float32)

def Dropout(tensor, rate=0.05, r=0, transcript=None, clock=0.0):
rate
= tf_pick_value_from_range(rate, r, clock=clock)
rate
= tf.math.maximum(0.0, rate)
factors
= tf.random.stateless_uniform(tf.shape(tensor),
(clock
* tf.int32.min, clock * tf.int32.max),
minval
=0.0,
maxval
=1.0,
dtype
=tf.float32)
return tensor * tf.math.sign(tf.math.floor(factors + rate))

添加增强

将从正态分布(均值为 0.0)中选取的随机值添加到目标数据表示的所有数据点。代码修改自:DeepSpeech

def tf_pick_value_from_range(value, r, clock=None, double_precision=False):
    clock = (tf.random.stateless_uniform([], seed=(-1, 1), dtype=tf.float64) if clock is None
             else tf.maximum(tf.constant(0.0, dtype=tf.float64), tf.minimum(tf.constant(1.0, dtype=tf.float64), clock)))
    value = tf.random.stateless_uniform([],
                                        minval=value - r,
                                        maxval=value + r,
                                        seed=(clock * tf.int32.min, clock * tf.int32.max),
                                        dtype=tf.float64)
    if isinstance(value, int):
        return tf.cast(tf.math.round(value), tf.int64 if double_precision else tf.int32)
    return tf.cast(value, tf.float64 if double_precision else tf.float32)

def Add(tensor, stddev=5, r=0, transcript=None, clock=0.0):
stddev
= tf_pick_value_from_range(stddev, r, clock=clock)
seed
= (clock * tf.int32.min, clock * tf.int32.max)
return tensor + tf.random.stateless_normal(tf.shape(tensor), seed, mean=0.0, stddev=stddev)

乘法增强

将目标数据表示的所有数据点与从正态分布(均值为 1.0)中选取的随机值相乘。代码修改自:DeepSpeech

def tf_pick_value_from_range(value, r, clock=None, double_precision=False):
    clock = (tf.random.stateless_uniform([], seed=(-1, 1), dtype=tf.float64) if clock is None
             else tf.maximum(tf.constant(0.0, dtype=tf.float64), tf.minimum(tf.constant(1.0, dtype=tf.float64), clock)))
    value = tf.random.stateless_uniform([],
                                        minval=value - r,
                                        maxval=value + r,
                                        seed=(clock * tf.int32.min, clock * tf.int32.max),
                                        dtype=tf.float64)
    if isinstance(value, int):
        return tf.cast(tf.math.round(value), tf.int64 if double_precision else tf.int32)
    return tf.cast(value, tf.float64 if double_precision else tf.float32)

def Multiply(self, tensor, stddev=5, r=0, transcript=None, clock=0.0):
stddev
= tf_pick_value_from_range(stddev, r=0, clock=clock)
seed
= (clock * tf.int32.min, clock * tf.int32.max)
return tensor * tf.random.stateless_normal(tf.shape(tensor), seed, mean=1.0, stddev=stddev)

通道 Shuffle

打乱原先通道顺序,这可以帮助对抗位置偏见。但是需要考虑打乱后会不会破坏麦克风阵列原始排布顺序,举个例子:

双麦:01,10 可以随机打乱

三麦 (等间隔环形):012、120、201、210、102、021。这 6 个位置都是等价的。

3 麦 (等间隔线性):012 和 120 就完全不一样了,只能 012,210 两种

def ChannelShuffle(wav, ratio=0.7):
    """ 打乱通道顺序
    Args:
        wav: (T, C)
        ratio: 打乱的概率
    Returns:
    """
    if random.random() < ratio:
        channel_num = wav.shape[-1]
        shuffled_channel_indexes = list(range(channel_num))
        random.shuffle(shuffled_channel_indexes)
        wav = wav[..., shuffled_channel_indexes]
    return wav

参考

【python 音频处理库】

【知乎文章】简单地为语音加混响

【国际音频实验室 EmanuëlHabets 提供的代码】International Audio Laboratories Erlangen

【Image-source method】Image-source method for room acoustics

【Image-source 原理讲解】Image-source Model

【CSDN】变速变调原理与方法总结

【CSDN】音频倍速(变速不变调)的实现

【CSDN】音频变调算法小结

【CSDN】python 音频变调不变速方法

【论文】用于语音识别的音频增强

【论文】SpecAugment: 一种简单的自动语音识别数据增强方法

【CSDN】SoX 音频处理工具使用方法

还有写没有跑通,但是总感觉有些价值的代码,记录在这里:

  • py-RIR-Generator(没跑通的原因是我是 window 系统)
  • gpuRIR(这个我跑通了,但是需要较大的计算资源)
  • 去 github 找代码的时候,不一定要搜索“回声”,“混响”,也可以通过搜索 "RIR" 同样可以得到想要的结果

本文画图代码:

# Author: 凌逆战
# -*- coding:utf-8 -*-
import matplotlib.pyplot as plt
import librosa
import numpy as np

plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示符号

y1, _ = librosa.load("./speech.wav", sr=16000)
y2, _
= librosa.load("./guitar_16k_reverb_ISM.wav", sr=16000)

plt.subplot(2, 2, 1)
plt.specgram(y1, Fs
=16000, scale_by_freq=True, sides='default', cmap="jet")
plt.title(
"语谱图", fontsize=13)
plt.xlabel(
'时间 /s', fontsize=13)
plt.ylabel(
'频率 /Hz', fontsize=13)

plt.subplot(2, 2, 2)
plt.plot(np.arange(len(y1))
/ 16000, y1)
plt.title(
"波形图", fontsize=13)
plt.xlabel(
'时间 /s', fontsize=13)
plt.ylabel(
'振幅', fontsize=13)

plt.subplot(2, 2, 3)
plt.specgram(y2, Fs
=16000, scale_by_freq=True, sides='default', cmap="jet")
plt.title(
"语谱图 (加混响)", fontsize=13)
plt.xlabel(
'时间 /s', fontsize=13)
plt.ylabel(
'频率 /Hz', fontsize=13)

plt.subplot(2, 2, 4)
plt.plot(np.arange(len(y2))
/ 16000, y2)
plt.title(
"波形图 (加混响)", fontsize=13)
plt.xlabel(
'时间 /s', fontsize=13)
plt.ylabel(
'振幅', fontsize=13)

plt.tight_layout()
plt.show()

View Code

[1] J,B. Allen and D. A. Berkley, Image method for efficiently simulating small-room acoustics, J. Acoust. Soc. Am., vol. 65, no. 4, p. 943, 1979.

[2] M, Vorlaender, Auralization, 1st ed. Berlin: Springer-Verlag, 2008, pp. 1-340.

[3] D.Schroeder, Physically based real-time auralization of interactive virtual environments. PhD Thesis, RWTH Aachen University, 2011.

 

作者:凌逆战
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
本文章不做任何商业用途,仅作为自学所用,文章后面会有参考链接,我可能会复制原作者的话,如果介意,我会修改或者删除。