从空气震动到梅尔频谱图

导库:

1
2
3
4
5
import torch
import torchaudio
import matplotlib.pyplot as plt
import os
import numpy as np

空气振动 ===> 时域波形

声波的采样

声音通过空气传播,被麦克风采集,存储为音频文件,其中一种格式叫做.wav。
声波是模拟量,是连续的,要将其转化为数字量就需要采样。并且这个转化一定会有损失存在。
一共涉及到两个重要参数,一个是采样率,另一个是采样深度。采样率越高,单位时间采集的点就越多,采样深度越大,点的大小刻度就越细。一般想要还原一段音频,采样率必须高于这段音频最大频率分量的2倍。麦克风硬件完成模拟量到数字量的转换。

用torchaudio 读取.wav 文件可以得到:

1
2
wave_path = "./bed_0b56bcfe_nohash_0.wav"
print(torchaudio.info(wave_path))
1
AudioMetaData(sample_rate=16000, num_frames=16000, num_channels=1, bits_per_sample=16, encoding=PCM_S)

表示这个文件保存的音频数据的采样率为16000Hz, 总共有16000 个点,所以时长为1s.那么里面的音频数据具体是怎样的呢?数据位数为16,那数据类型呢,可以是有符号整数也可以是无符号。encoding=PCM_S, 中的S 就表示signed.所以每个数据点的范围为[-32768, 32767].

读取wav 文件并可视化

有了点的个数和点的值就能画出这个wav 文件所代表的时域波形。

先利用torchaudio 解析wav 文件,转化为tensor:

1
2
3
4
wave = torchaudio.load(wave_path, normalize=False)
print(wave)
wave = torchaudio.load(wave_path, normalize=True)
print(wave)
1
2
(tensor([[ 6, 11, 15,  ...,  7, 12, 11]], dtype=torch.int16), 16000)
(tensor([[0.0002, 0.0003, 0.0005, ..., 0.0002, 0.0004, 0.0003]]), 16000)

这里参数normalize 默认为True, 会将样本点的数据类型转化到torch.float32 并且缩放到[-1, 1]
画图:

1
2
3
4
5
6
7
8
9
wave = []
wave.append(torchaudio.load(wave_path, normalize=False))
wave.append(torchaudio.load(wave_path, normalize=True))

fig, axes = plt.subplots(1, 2, figsize=(12, 7))
x = torch.arange(1, 16000+1, 1)
for index, ax in enumerate(axes):
y = wave[index][0][0]
ax.plot(x, y)

alt text
左边是没有缩放和标准化的,右边是是标准化后的。由于在同一个词语,在不同响度的情况下,波形幅度大小不同,但它们都是同一个词语,所以我们一般采用标准化后的波形。

时域波形 ===> 频谱图

与上文同思路,找出画出频谱图的必须条件,比如:横纵坐标以及数据点的具体值,就能画出频谱图,画出来了,也就理解了。
傅里叶变换具体原理,我个人还不太理解,先插个眼在这里,如果以后有契机再补全吧。傅里叶变换原理
频谱图表示一段音频中各个频率的分量的大小。横坐标就是频率,范围就是0hz 到采样频率(FS),分辨率就是采样率(FS)除以样本点个数(N)。纵坐标分两种,功率谱时为功率,能量谱时为能量。
这里没有明白为什么对时域信号进行傅里叶变换后,得到的横坐标长度为采样频率,以及纵坐标为功率和能量,估计是因为没有去具体学习傅里叶变换。
对上文的wave 进行傅里叶变换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
specgram = []
for i in wave:
specgram.append(torch.fft.fft(i[0][0]))
for i in specgram:
print(len(i))
print(torch.abs(i))

fig, axes = plt.subplots(2, 2, figsize=(15, 8))
x = torch.arange(1, 16000+1, 1)
for index, ax in enumerate(axes[0]):
y = specgram[index]
ax.plot(x, y)

x = torch.arange(1, 16000//2+1, 1)
for index, ax in enumerate(axes[1]):
y = specgram[index][0:16000//2]
ax.plot(x, y)
1
2
3
4
5
16000
tensor([114381.0000, 113926.7969, 111315.3984, ..., 107403.2188,
111315.3984, 113926.7969])
16000
tensor([3.4906, 3.4768, 3.3971, ..., 3.2777, 3.3971, 3.4768])

alt text
首先看横坐标,傅里叶变换后的横坐标个数就等于变换前样本点的个数,只不过数据左右对称,所以一般只取一半。横坐标的最小间距是采样率(FS)/样本点个数,只是这里恰好等于1而已。
然后就是纵坐标,这里的纵坐标主要跟变换前的时域信号相关,没有标准化的纵坐标非常大,标准化后的相对较小,一般取标准化后的。
这里我会有直觉上的疑惑:假如我对一个幅值为10 的正弦波进行傅里叶变换,那么变换后的频谱图(振幅图)中对应的尖刺峰值应该为10 才对。但是经过实验发现,峰值为几百。查询资料发现,这个峰值包含了多个样本点的能量.处理方法如下:
直流分量(0 Hz):得到的振幅需要除以N,N 为样本点个数
非直流分量:得到的振幅需要除以(N/2),N 为样本点个数

频谱图 ===> 时频图

对于两个读音完全不相同的词语来说,他们的时域图肯定不相同,但是频谱图可能相同(极端情况)。虽然完全相同的情况不多,但相似的情况绝对不少。对于关键词分类来说,频谱图的信息诚然比时域图的多,但这种相似情况却让其不能作为分类模型的输入。所以时频图诞生了,它既保留了频域信号有包含时域信号,是折中的选择。
大致流程是,规定一个长度,从时域图上等距取出此长度的数据(这些数据可以重合),每个此长度的数据简称为窗口。每个窗口都包含一部分时域数据,窗口与窗口之间有重合部分。对这些窗口分别进行傅里叶变换,得到这一小段时间内的频谱图,再将所有的二维频谱图合成一张三维图片,这张图片就是时频图。

1
2
3
4
5
6
7
8
# 这里是直接从时域图到时频图,一步到位
print(f"时域信号:{wave[1][0][0]}")
n_fft = 600
hop_length = 300
window_fn = torch.hann_window
spectrogram = torchaudio.transforms.Spectrogram(n_fft=n_fft, hop_length=hop_length, window_fn=window_fn, power=1, center=False)
one_specgram = spectrogram(wave[1][0][0])
print(f"频域信号:{one_specgram.shape}")
1
2
时域信号:tensor([0.0002, 0.0003, 0.0005,  ..., 0.0002, 0.0004, 0.0003])
频域信号:torch.Size([301, 52])

窗口大小为600,所以单个傅里叶变换的样本个数就为600,所以频率个数就为301,而频率最小间隔就为:16000/600 = 26.66 Hz.窗口大小为600,窗口滑动步长为300,可以计算得到窗口个数为52(剩下的数据不够一个窗口,舍去)。
画图:

1
2
3
4
5
fig, axes = plt.subplots(1, 1, figsize=(20, 5))
axes.imshow(one_specgram, aspect="auto", origin="lower", cmap="hot", extent=[0, one_specgram.shape[1], 0, int(one_specgram.shape[0]*(16000/600))])
axes.set_title("Spectrogram Visualization")
axes.set_xlabel("Time Frames")
axes.set_ylabel("Frequency")

alt text
这里的三维图片的具体数据是由标准化到[-1, 1]的时域数据傅里叶变换而来,这些数据差距非常大(有的分量的值非常大,有的非常小),这就导致较小的值之间的差距很小所以全为黑色。下面通过对数变换一下,将差距减小,以便于分辨这些细小的差距。

1
2
3
4
5
fig, axes = plt.subplots(1, 1, figsize=(20, 5))
axes.imshow(10*torch.log10(one_specgram), aspect="auto", origin="lower", cmap="hot", extent=[0, one_specgram.shape[1], 0, int(one_specgram.shape[0]*(16000/600))])
axes.set_title("Spectrogram Visualization")
axes.set_xlabel("Time Frames")
axes.set_ylabel("Frequency")

alt text

梅尔频谱图

。。。。。。。