前言
玩过CV的都知道猫狗识别,通过输入一张猫狗图片之后经过神经网络就能知道这张图片属于猫还是狗,图像识别的输入是很直观的,这源于我们对图像理性的认识(RGB图像有三个通道,每个通道中每个像素点都是一个数字…)。但声音显然是一个朦胧的概念,声音是如何实现分类的呢?CV有人脸识别,声音也存在声纹识别,这些在人工智能里面都是如何判断的,让我们从一篇简单的声音分类开始吧!
音频简介
现在正式开启语音的学习历程,语音识别下面有很多任务,例如中文识别、文字转语音、语音唤醒以及说话人识别(声纹识别)等。我观察了很多demo,他们的语音识别所用的语音数据集文件基本上都是.wav文件,就算原始语音不是wav也会转为wav,所以我们首先了解一下wav文件是什么,以及其中的组成。
WAV是什么?
WAV格式是微软公司开发的一种声音文件格式,也叫波形声音文件,是最早的数字音频格式,被Windows平台及其应用程序广泛支持。WAV格式支持许多压缩算法,支持多种音频位数、采样频率和声道,采用44.1kHz的采样频率,16位量化位数,因此WAV的音质与CD相差无几,通常被称为无损音频。
采样率、位深
对于音频数据,比较重要的两个概率就是采样率和位深了,在此简单介绍一下这两个概念,以对声音的产生有一个直观的感受。先对他们有一个宏观的认识:声波,有频率和振幅,频率高低决定音调,振幅大小决定响度,采样率是对频率采样,位深是对振幅采样。
音频信号是一种连续的模拟信号,计算机不可能处理这种连续的模拟数据。它首先需要转换为一系列离散值,而“采样”就是这样做的。“采样率”和“位深”是离散化音频信号时最重要的两个要素。在下图中,我们可以看到它们与模数转换的关系。在图中,x 轴是时间,y 轴是幅度。“采样率”决定采样的频率,“位深度”决定采样的详细程度。
因此采样率是指:声音信号在“模→数”转换过程中单位时间内采样的次数,也就是每秒钟所采样样本的总数目是采样率。而每个样本中信息的比特数就是位深。
可能还是比较抽象,此处借助一个具体的例子来解释:
经常见到这样的描述: 44100HZ 16bit stereo 或者 22050HZ 8bit mono 等等.
44100HZ 16bit stereo: 每秒钟有 44100 次采样(采样率), 采样数据(即位深)用 16 位(2字节)记录, 双声道;
22050HZ 8bit mono: 每秒钟有 22050 次采样, 采样数据用 8 位(1字节)记录, 单声道;
每个采样数据记录的是振幅, 采样精度取决于储存空间的大小:
- 1 字节(也就是8bit) 只能记录 256 个数, 也就是只能将振幅划分成 256 个等级;
- 2 字节(也就是16bit) 可以细到 65536 个数, 这已是 CD 标准了;
- 4 字节(也就是32bit) 能把振幅细分到 4294967296 个等级, 实在是没必要了;
如果是双声道(stereo), 采样就是双份的, 文件也差不多要大一倍。
人对频率的识别范围是 20HZ - 20000HZ, 如果每秒钟能对声音做 20000 个采样, 回放时就足可以满足人耳的需求。 所以 22050 的采样频率是常用的, 44100已是CD音质, 超过48000的采样对人耳已经没有意义。这一点在后面声音处理时会用到。
声音处理以及可视化
上面已经简单介绍了声音的基本知识,那么就来到我们的声音处理环节,可视化声音的波纹。首先引入我使用的第一个数据集:UrbanSound8K,可以点此处直接下载。如果使用的是服务器,可以使用wget下载解压,命令如下:
wget https://zenodo.org/record/1203745/files/UrbanSound8K.tar.gz
下载完成之后解压到指定文件夹:
tar -zxvf UrbanSound8K.tar.gz -C /media/data/xxx/Speech_Recognition/
首先在Python中元数据的格式,其是一个csv文件,包含了所有声音文件的信息:
import pandas as pd
data = pd.read_csv("../Dataset/UrbanSound8K/metadata/UrbanSound8K.csv")
print(data)
print(data.head())
print(data.shape)
结果为:
slice_file_name fsID start ... fold classID class
0 100032-3-0-0.wav 100032 0.000000 ... 5 3 dog_bark
1 100263-2-0-117.wav 100263 58.500000 ... 5 2 children_playing
2 100263-2-0-121.wav 100263 60.500000 ... 5 2 children_playing
3 100263-2-0-126.wav 100263 63.000000 ... 5 2 children_playing
4 100263-2-0-137.wav 100263 68.500000 ... 5 2 children_playing
... ... ... ... ... ... ... ...
8727 99812-1-2-0.wav 99812 159.522205 ... 7 1 car_horn
8728 99812-1-3-0.wav 99812 181.142431 ... 7 1 car_horn
8729 99812-1-4-0.wav 99812 242.691902 ... 7 1 car_horn
8730 99812-1-5-0.wav 99812 253.209850 ... 7 1 car_horn
8731 99812-1-6-0.wav 99812 332.289233 ... 7 1 car_horn
[8732 rows x 8 columns]
slice_file_name fsID start ... fold classID class
0 100032-3-0-0.wav 100032 0.0 ... 5 3 dog_bark
1 100263-2-0-117.wav 100263 58.5 ... 5 2 children_playing
2 100263-2-0-121.wav 100263 60.5 ... 5 2 children_playing
3 100263-2-0-126.wav 100263 63.0 ... 5 2 children_playing
4 100263-2-0-137.wav 100263 68.5 ... 5 2 children_playing
[5 rows x 8 columns]
(8732, 8)
- slice_file_name:音频文件的名称
- fsID:摘录的录音的 FreesoundID
- start:切片的开始时间
- end:切片的结束时间
- salience:声音的显着性等级。1 = 前景,2 = 背景
- fold:此文件已分配到的折叠编号(1-10)
- classID:
0 = air_conditioner
1 = car_horn
2 = children_playing
3 = dog_bark
4 = drilling
5 = engine_idling
6 = gun_shot
7 = jackhammer
8 = siren
9 = street_music - class:类别名称
接下来来看看数据的分布情况:
appended = []
for i in range(1, 11):
appended.append(data[data.fold == i]['class'].value_counts())
class_distribution = pd.DataFrame(appended)
class_distribution = class_distribution.reset_index()
class_distribution['index'] = ["fold" + str(x) for x in range(1, 11)]
print(class_distribution)
结果为:
index jackhammer children_playing ... siren car_horn gun_shot
0 fold1 120 100 ... 86 36 35
1 fold2 120 100 ... 91 42 35
2 fold3 120 100 ... 119 43 36
3 fold4 120 100 ... 166 59 38
4 fold5 120 100 ... 71 98 40
5 fold6 68 100 ... 74 28 46
6 fold7 76 100 ... 77 28 51
7 fold8 78 100 ... 80 30 30
8 fold9 82 100 ... 82 32 31
9 fold10 96 100 ... 83 33 32
通过最后两列可以发现,数据的分布是不平衡的。更直观的观察是每类的频率:
print(data['class'].value_counts(normalize=True))
其结果为:
[10 rows x 11 columns]
dog_bark 0.114521
children_playing 0.114521
street_music 0.114521
engine_idling 0.114521
drilling 0.114521
jackhammer 0.114521
air_conditioner 0.114521
siren 0.106390
car_horn 0.049130
gun_shot 0.042831
Name: class, dtype: float64
以上就是数据的具体信息,那么接下来就是对wav文件的详细分解。
首先可视化一个声音文件:
import os
import struct
import pandas as pd
from scipy.io import wavfile as wav
import matplotlib.pyplot as plt
data = pd.read_csv("../Dataset/UrbanSound8K/metadata/UrbanSound8K.csv")
def path_class(filename):
excerpt = data[data['slice_file_name'] == filename]
path_name = os.path.join('../Dataset/UrbanSound8K/audio', 'fold' + str(excerpt.fold.values[0]), filename)
return path_name, excerpt['class'].values[0]
def wav_plotter(full_path, class_label):
rate, wav_sample = wav.read(full_path)
print(wav_sample)
wave_file = open(full_path, "rb")
riff_fmt = wave_file.read(36)
bit_depth_string = riff_fmt[-2:]
bit_depth = struct.unpack("H", bit_depth_string)[0]
print('sampling rate: ', rate, 'Hz')
print('bit depth: ', bit_depth)
print('number of channels: ', wav_sample.shape[1])
print('duration: ', wav_sample.shape[0] / rate, ' second')
print('number of samples: ', len(wav_sample))
print('class: ', class_label)
plt.figure(figsize=(12, 4))
plt.plot(wav_sample)
plt.show()
fullpath, label = path_class('100263-2-0-117.wav')
wav_plotter(fullpath, label)
可视化结果:
那么从这幅图中我们可以看到采样数据大约17600个,而这段音频我已经知道大约四秒左右,也就是说这段音频的采样率应该是44100HZ,而振幅很显然是大于2000的,那么其位深就可以猜测为16bit(32bit很少),并且我们可以看到图中有两种颜色的图,也就对应了双声道,是一个立体声。
那么结果是否准确,我们可以通过上面的代码获得具体结果:
sampling rate: 44100 Hz
bit depth: 16
number of channels: 2
duration: 4.0 second
number of samples: 176400
class: children_playing
但此处有一个问题,就是Urbansound8K有一个注释说“8732个WAV格式的城市声音音频文件。采样率、位深度和通道数与上传到 Freesound 的原始文件相同(因此可能因文件而异)。”这意味着数据中可能有许多不同的采样率。此外,不同的位深度意味着它们可以取不同的值范围。其中一些可能是立体声,而另一些则是单声道。这些都很难处理。
那么接下来我们就可以统计这个数据集的每种采样率,位深共包含多少声音文件,在统计之前,我们首先需要了解一下wav文件的组成, 一幅图概括:
那么从这幅图可以很清晰的看出声道数量,采样频率,位深(采样位数)所对应的字段,接下来就可以获取具体的信息:
def wav_fmt_parser(file_name):
full_path, _ = path_class(file_name)
wave_file = open(full_path, "rb")
riff_fmt = wave_file.read(36) #只读取前36位信息
n_channels_string = riff_fmt[22:24] # 通道数
n_channels = struct.unpack("H", n_channels_string)[0]
s_rate_string = riff_fmt[24:28] # 采样率
s_rate = struct.unpack("I", s_rate_string)[0]
bit_depth_string = riff_fmt[-2:] # 倒数两位即位深
bit_depth = struct.unpack("H", bit_depth_string)[0]
return n_channels, s_rate, bit_depth
接下来就是遍历所有声音文件进行信息的捕捉:
wav_fmt_data = [wav_fmt_parser(i) for i in data.slice_file_name]
这样获得的信息全在一个元组里面,数据格式为:
(2, 44100, 16), (2, 44100, 16), (2, 44100, 16), (2, 44100, 16), (2, 44100, 16)
将其利用pandas加入到data原始数据:
data[['n_channels', 'sampling_rate', 'bit_depth']] = pd.DataFrame(wav_fmt_data)
此时得到:
slice_file_name fsID ... sampling_rate bit_depth
0 100032-3-0-0.wav 100032 ... 44100 16
1 100263-2-0-117.wav 100263 ... 44100 16
2 100263-2-0-121.wav 100263 ... 44100 16
3 100263-2-0-126.wav 100263 ... 44100 16
4 100263-2-0-137.wav 100263 ... 44100 16
... ... ... ... ... ...
8727 99812-1-2-0.wav 99812 ... 44100 16
8728 99812-1-3-0.wav 99812 ... 44100 16
8729 99812-1-4-0.wav 99812 ... 44100 16
8730 99812-1-5-0.wav 99812 ... 44100 16
8731 99812-1-6-0.wav 99812 ... 44100 16
注意看后几列,之后统计每个采样率的音频数量:
print(data.sampling_rate.value_counts())
print(data.n_channels.value_counts())
print(data.bit_depth.value_counts())
结果为:
[8732 rows x 11 columns]
44100 5370
48000 2502
96000 610
24000 82
16000 45
22050 44
11025 39
192000 17
8000 12
11024 7
32000 4
Name: sampling_rate, dtype: int64
2 7993
1 739
Name: n_channels, dtype: int64
16 5758
24 2753
32 169
8 43
4 9
Name: bit_depth, dtype: int64
可以发现数据的格式还是非常混乱的,如果不做归一化处理可能无法直接使用。那么接下来就是对数据进行转换以获得可以在同一情况下直接比对的数据。所幸万能的Python有着一种直接的方法对数据进行转换,名为:Librosa。
默认情况下,Librosa 的加载函数会将采样率转换为 22.05khz,并将通道数减少到 1(单声道),并对数据进行归一化,使值范围从 -1 到 1。
fullpath, class_name = path_class('100652-3-0-1.wav')
librosa_load, librosa_sampling_rate = librosa.load(fullpath)
print('converted sample rate:', librosa_sampling_rate)
print('converted wav file min~max range:', np.min(librosa_load), '~', np.max(librosa_load))
此时结果为:
converted sample rate: 22050
converted wav file min~max range: -0.7296108 ~ 0.74331266
而原始结果:
scipy_sampling_rate, scipy_load = wav.read(fullpath)
print('original sample rate:', scipy_sampling_rate)
print('original wav file min~max range:', np.min(scipy_load), '~', np.max(scipy_load))
original sample rate: 44100
original wav file min~max range: -30926 ~ 30119
我们可以将转换后的数据和原始数据的左右声道都绘制出来:
plt.subplot(3, 1, 1)
plt.plot(librosa_load)
plt.subplot(3, 1, 2)
plt.plot(scipy_load[:, 0])
plt.subplot(3, 1, 3)
plt.plot(scipy_load[:, 1])
plt.show()
可以看到结果图为:
从这幅图中我们可以粗略看出,该工具很好的将原始数据在不损失其质量的前提下转化成功。OK,本节结束,对此声音的特征提取和网络的分类可以见下节。
参考博客:
- https://www.cnblogs.com/ranson7zop/p/7657874.html
- https://blog.csdn.net/cindywry/article/details/108244610
- https://towardsdatascience.com/urban-sound-classification-part-2-sample-rate-conversion-librosa-ba7bc88f209a