音频信号的读写、播放及录音
标准的python已经支持WAV格式的书写,而实时的声音输入输出需要安装pyAudio()。最后我们还将使用pyMedia()进行Mp3的解码和播放。
音频信号是模拟信号,我们需要将其保存为数字信号,才能对语音进行算法操作,WAV是Microsoft开发的一种声音文件格式,通常被用来保存未压缩的声音数据。
语音信号有三个重要的参数:声道数、取样频率和量化位数。
- 声道数:可以是单声道或者是双声道
- 采样频率:一秒内对声音信号的采集次数,44100Hz采样频率意味着每秒钟信号被分解成44100份。换句话说,每隔$\frac{1}{44100}Hz$就会存储一次,如果采样率高,那么媒体播放音频时会感觉信号是连续的。
- 量化位数:用多少bit表达一次采样所采集的数据,通常有8bit、16bit、24bit和32bit等几种
例如CD中所储存的声音信号是双声道、44.1kHz、16bit。
如果你需要自己录制和编辑声音文件,推荐使用Audacity(),它是一款开源的、跨平台、多声道的录音编辑软件。在我的工作中经常使用Audacity进行声音信号的录制,然后再输出成WAV文件供Python程序处理。
wave-读wav文件
wava模块为WAV声音格式提供了方面的界面,他不支持压缩/解压,但支持单声道/立体声。
Wave_read = wave.open(file,mode="rb")
file通常为是字符串格式的文件名或者文件路径
例如voice.wav文件的路径C:\Users\Never\Desktop\code for the speech
则file有以下三种填写格式:
r"C:\Users\Never\Desktop\code for the speech\voice.wav"
"C:/Users/Never/Desktop/code for the speech/voice.wav"
"C:\\Users\\Never\\Desktop\\code for the speech\\voice.wav"
三者等价,右划线\为转意字符,如果要表达\则需要\\,引号前面加r表示原始字符串。
mode是缺省参数,可以不填,也可以是"rb":只读模式;"wb":只写模式。注意不支持读/写格式。
该函数可用于声明中。当块完成时,或方法被调用。Wave_read是读取的文件流。
Wave_read.
getparams
()
一次性返回所有的音频参数,返回的是一个元组(声道数,量化位数(byte单位),采样频率,采样点数,压缩类型,压缩类型的描述)。(nchannels, sampwidth, framerate, nframes, comptype, compname)wave模块只支持非压缩的数据,因此可以忽略最后两个信息。
str_data = Wave_read.
readframes
(nframes)
指定需要读取的长度(以取样点为单位),返回的是字符串类型的数据
wave_data = np.fromstring(str_data, dtype=np.short)
将读取的字符串数据转换为一维short类型的数组。
通过np.fromstring函数将字符串转换为数组,通过其参数dtype指定转换后的数据格式(由于我们的声音格式是以两个字节表示一个取样值,因此采用short数据类型转换)
现在的wave_data是一个一维的short类型的数组,但是因为我们的声音文件是双声道的,因此它由左右两个声道的取样交替构成:LR
wave_data.shape = (-1, 2) # -1的意思就是没有指定,根据另一个维度的数量进行分割,得到n行2列的数组。
getnchannels, getsampwidth, getframerate, getnframes等方法可以单独返回WAV文件的特定的信息。
Wave_read.
close
() 关闭文件流
Wave_read.
getnchannels
() 返回音频通道的数量(1
对于单声道,2
对于立体声)。 Wave_read.
getsampwidth
() 以字节为单位返回样本宽度 Wave_read.
getframerate
() 返回采样频率。 Wave_read.
getnframes
() 返回音频帧数。 Wave_read.
rewind
() 将文件指针倒回到音频流的开头。 Wave_read.
tell
() 返回当前文件指针位置。 读取通道数为2的音频信号 # -*- coding: utf-8 -*-# 读Wave文件并且绘制波形import waveimport matplotlib.pyplot as pltimport numpy as np# 打开WAV音频f = wave.open(r"C:\Windows\media\Windows Background.wav", "rb")# 读取格式信息# (声道数、量化位数、采样频率、采样点数、压缩类型、压缩类型的描述)# (nchannels, sampwidth, framerate, nframes, comptype, compname)params = f.getparams()nchannels, sampwidth, framerate, nframes = params[:4]# nchannels通道数 = 2# sampwidth量化位数 = 2# framerate采样频率 = 22050# nframes采样点数 = 53395# 读取nframes个数据,返回字符串格式str_data = f.readframes(nframes)f.close()#将字符串转换为数组,得到一维的short类型的数组wave_data = np.fromstring(str_data, dtype=np.short)# 赋值的归一化wave_data = wave_data*1.0/(max(abs(wave_data)))# 整合左声道和右声道的数据wave_data = np.reshape(wave_data,[nframes,nchannels])# wave_data.shape = (-1, 2) # -1的意思就是没有指定,根据另一个维度的数量进行分割# 最后通过采样点数和取样频率计算出每个取样的时间time = np.arange(0, nframes) * (1.0 / framerate)plt.figure()# 左声道波形plt.subplot(3,1,1)plt.plot(time, wave_data[:,0])plt.xlabel("time (seconds)")plt.ylabel("Amplitude")plt.title("Left channel")plt.grid() # 标尺plt.subplot(3,1,3)# 右声道波形plt.plot(time, wave_data[:,1], c="g")plt.xlabel("time (seconds)")plt.ylabel("Amplitude")plt.title("Left channel")plt.title("right channel")plt.grid()plt.show()
效果图:
第二种读取文件的方式:
from scipy.io import wavfile
sampling_freq, audio = wavfile.read("***.wav")
audio 是直接经过归一化的数组
第三种读取音频的方式:
import librosa
y, sr = librosa.load(filename)
该函数是会改变声音的采样频率的。如果 sr 缺省,librosa.load()会默认以22050的采样率读取音频文件,高于该采样率的音频文件会被下采样,低于该采样率的文件会被上采样。因此,如果希望以原始采样率读取音频文件,sr 应当设为 None。具体做法为 y, sr = librosa(filename, sr=None)。
audio 是直接经过归一化的数组
wave-写wav音频
在写入第一帧数据时,先通过调用设置好帧数,setnchannels()设置好声道数,setsampwidth()设置量化位数,setframerate()设置好采样频率,
然后用于写入帧数据。
Wave_write = wave.open(file,mode="wb")
Wave_write是写文件流,
Wave_write.
setnchannels
(n) 设置通道数。
Wave_write.
setsampwidth
(n) 将样本宽度设置为n个字节,量化位数
Wave_write.
setframerate
(n) 将采样频率设置为n。
Wave_write.
setnframes
(n) 将帧数设置为n
Wave_write.
setparams
(tuple) 以元组形式设置所有参数(nchannels, sampwidth, framerate, nframes,comptype, compname)
Wave_write.
writeframes
(data) 写入data个长度的音频,以采样点为单位
Wave_write.
tell
() 返回文件中的当前位置
# -*- coding: utf-8 -*-import waveimport numpy as npimport scipy.signal as signalframerate = 44100 # 采样频率time = 10 # 持续时间t = np.arange(0, time, 1.0/framerate)# 调用scipy.signal库中的chrip函数,# 产生长度为10秒、取样频率为44.1kHz、100Hz到1kHz的频率扫描波wave_data = signal.chirp(t, 100, time, 1000, method='linear') * 10000# 由于chrip函数返回的数组为float64型,# 需要调用数组的astype方法将其转换为short型。wave_data = wave_data.astype(np.short)# 打开WAV音频用来写操作f = wave.open(r"sweep.wav", "wb")f.setnchannels(1) # 配置声道数f.setsampwidth(2) # 配置量化位数f.setframerate(framerate) # 配置取样频率comptype = "NONE"compname = "not compressed"# 也可以用setparams一次性配置所有参数# outwave.setparams((1, 2, framerate, nframes,comptype, compname))# 将wav_data转换为二进制数据写入文件f.writeframes(wave_data.tostring())f.close()
import waveimport numpy as npimport structf = wave.open(r"C:\Windows\media\Windows Background.wav", "rb")params = f.getparams()nchannels, sampwidth, framerate, nframes = params[:4]strData = f.readframes(nframes)waveData = np.fromstring(strData,dtype=np.int16)f.close()waveData = waveData*1.0/(max(abs(waveData)))# wav文件写入# 待写入wav的数据,这里仍然取waveData数据outData = waveDataoutwave = wave.open("write.wav", 'wb')nchannels = 1 # 通道数设置为1sampwidth = 2 # 量化位数设置为2framerate = 8000 # 采样频率8000nframes = len(outData) # 采样点数comptype = "NONE"compname = "not compressed"outwave.setparams((nchannels, sampwidth, framerate, nframes, comptype, compname))for i in outData: outwave.writeframes(struct.pack('h', int(i * 64000 / 2))) # struct.pack(FMT, V1)将V1的值转换为FMT格式字符串outwave.close()
第二种写音频文件的方法
from scipy.io.wavfile import write
write(output_filename, freq, audio)
import numpy as npimport matplotlib.pyplot as pltfrom scipy.io.wavfile import write# 定义存储音频的输出文件output_file = 'output_generated.wav'# 指定音频生成的参数duration = 3 # 单位秒sampling_freq = 44100 # 单位Hztone_freq = 587 # 音调的频率min_val = -2 * np.pimax_val = 2 * np.pi# 生成音频信号t = np.linspace(min_val, max_val, duration * sampling_freq)audio = np.sin(2 * np.pi * tone_freq * t)# 添加噪声(duration * sampling_freq个(0,1]之间的随机值)noise = 0.4 * np.random.rand(duration * sampling_freq)audio += noisescaling_factor = pow(2,15) - 1 # 转换为16位整型数audio_normalized = audio / np.max(np.abs(audio)) # 归一化audio_scaled = np.int16(audio_normalized * scaling_factor) # 这句话什么意思write(output_file, sampling_freq, audio_scaled) # 写入输出文件audio = audio[:300] # 取前300个音频信号x_values = np.arange(0, len(audio), 1) / float(sampling_freq)x_values *= 1000 # 将时间轴单位转换为秒plt.plot(x_values, audio, color='blue')plt.xlabel('Time (ms)')plt.ylabel('Amplitude')plt.title('Audio signal')plt.show()
第三种写音频文件的方法
librosa.output.
write_wav
(path, y, sr, norm=False)
参数:
- path:str,保存输出wav文件的路径
- y:np.ndarry 音频时间序列
- sr:y的采样率
- norm:True/False,是否启动幅值归一化
合成有音调的音乐
import jsonimport numpy as npfrom scipy.io.wavfile import writeimport matplotlib.pyplot as plt# 定义合成音调def Synthetic_tone(freq, duration, amp=1.0, sampling_freq=44100): # 建立时间轴 t = np.linspace(0, duration, duration * sampling_freq) # 构建音频信号 audio = amp * np.sin(2 * np.pi * freq * t) return audio.astype(np.int16)# json文件中包含一些音阶以及他们的频率tone_map_file = 'tone_freq_map.json'# 读取频率映射文件with open(tone_map_file, 'r') as f: tone_freq_map = json.loads(f.read()) print(tone_freq_map)# {'A': 440, 'Asharp': 466, 'B': 494, 'C': 523, 'Csharp': 554, 'D': 587, 'Dsharp': 622, 'E': 659, 'F': 698, 'Fsharp': 740, 'G': 784, 'Gsharp': 831}# 设置生成G调的输入参数input_tone = 'G'duration = 2 # secondsamplitude = 10000 # 振幅sampling_freq = 44100 # Hz# 生成音阶synthesized_tone = Synthetic_tone(tone_freq_map[input_tone], duration, amplitude, sampling_freq)# 写入输出文件write('output_tone.wav', sampling_freq, synthesized_tone)# 音阶及其连续时间tone_seq = [('D', 0.3), ('G', 0.6), ('C', 0.5), ('A', 0.3), ('Asharp', 0.7)]# 构建基于和弦序列的音频信号output = np.array([])for item in tone_seq: input_tone = item[0] duration = item[1] synthesized_tone = Synthetic_tone(tone_freq_map[input_tone], duration, amplitude, sampling_freq) output = np.append(output, synthesized_tone, axis=0)# 写入输出文件write('output_tone_seq.wav', sampling_freq, output)
{ "A": 440, "Asharp": 466, "B": 494, "C": 523, "Csharp": 554, "D": 587, "Dsharp": 622, "E": 659, "F": 698, "Fsharp": 740, "G": 784, "Gsharp": 831}
音频播放
wav文件的播放用到的是pyaudio库
p = pyaudio.PyAudio()
stream = p.open(format = p.get_format_from_width(sampwidth),channels,rate,output = True)
stream.write(data) # 播放data数据
以下列出pyaudio对象的open()方法的主要参数:
rate - 取样频率
channels - 声道数
format - 取样值的量化格式 (paFloat32, paInt32, paInt24, paInt16, paInt8 ...)。在上面的例子中,使用get_format_from_width方法将wf.sampwidth()的返回值2转换为paInt16
input - 输入流标志,如果为True的话则开启输入流
output - 输出流标志,如果为True的话则开启输出流
input_device_index - 输入流所使用的设备的编号,如果不指定的话,则使用系统的缺省设备
output_device_index - 输出流所使用的设备的编号,如果不指定的话,则使用系统的缺省设备
frames_per_buffer - 底层的缓存的块的大小,底层的缓存由N个同样大小的块组成
start - 指定是否立即开启输入输出流,缺省值为True
# -*- coding: utf-8 -*-import pyaudioimport wavechunk = 1024wf = wave.open(r"c:\WINDOWS\Media\Windows Background.wav", 'rb')p = pyaudio.PyAudio()# 打开声音输出流stream = p.open(format = p.get_format_from_width(wf.getsampwidth()), channels = wf.getnchannels(), rate = wf.getframerate(), output = True)# 写声音输出流到声卡进行播放while True: data = wf.readframes(chunk) if data == "": break stream.write(data)stream.stop_stream()stream.close()p.terminate() # 关闭PyAudio
录音
以SAMPLING_RATE为采样频率,每次读入一块有NUM_SAMPLES个采样的数据块,当读入的采样数据中有COUNT_NUM个值大于LEVEL的取样的时候,将数据保存进WAV文件,一旦开始保存数据,所保存的数据长度最短为SAVE_LENGTH个块。WAV文件以保存时的时刻作为文件名。
从声卡读入的数据和从WAV文件读入的类似,都是二进制数据,由于我们用paInt16格式(16bit的short类型)保存采样值,因此将它自己转换为dtype为np.short的数组。
录音
'''以SAMPLING_RATE为采样频率,每次读入一块有NUM_SAMPLES个采样点的数据块,当读入的采样数据中有COUNT_NUM个值大于LEVEL的取样的时候,将采样数据保存进WAV文件,一旦开始保存数据,所保存的数据长度最短为SAVE_LENGTH个数据块。从声卡读入的数据和从WAV文件读入的类似,都是二进制数据,由于我们用paInt16格式(16bit的short类型)保存采样值,因此将它自己转换为dtype为np.short的数组。'''from pyaudio import PyAudio, paInt16import numpy as npimport wave# 将data中的数据保存到名为filename的WAV文件中def save_wave_file(filename, data): wf = wave.open(filename, 'wb') wf.setnchannels(1) # 单通道 wf.setsampwidth(2) # 量化位数 wf.setframerate(SAMPLING_RATE) # 设置采样频率 wf.writeframes(b"".join(data)) # 写入语音帧 wf.close()NUM_SAMPLES = 2000 # pyAudio内部缓存块的大小SAMPLING_RATE = 8000 # 取样频率LEVEL = 1500 # 声音保存的阈值,小于这个阈值不录COUNT_NUM = 20 # 缓存快类如果有20个大于阈值的取样则记录声音SAVE_LENGTH = 8 # 声音记录的最小长度:SAVE_LENGTH * NUM_SAMPLES 个取样# 开启声音输入pa = PyAudio()stream = pa.open(format=paInt16, channels=1, rate=SAMPLING_RATE, input=True, frames_per_buffer=NUM_SAMPLES)save_count = 0 # 用来计数save_buffer = [] #while True: # 读入NUM_SAMPLES个取样 string_audio_data = stream.read(NUM_SAMPLES) # 将读入的数据转换为数组 audio_data = np.fromstring(string_audio_data, dtype=np.short) # 计算大于LEVEL的取样的个数 large_sample_count = np.sum( audio_data > LEVEL ) print(np.max(audio_data)) # 如果个数大于COUNT_NUM,则至少保存SAVE_LENGTH个块 if large_sample_count > COUNT_NUM: save_count = SAVE_LENGTH else: save_count -= 1 if save_count < 0: save_count = 0 if save_count > 0: # 将要保存的数据存放到save_buffer中 save_buffer.append( string_audio_data ) else: # 将save_buffer中的数据写入WAV文件,WAV文件的文件名是保存的时刻 if len(save_buffer) > 0: filename = "recorde" + ".wav" save_wave_file(filename, save_buffer) print(filename, "saved") break
语音信号处理
语音信号的产生和感知
我们要对语音进行分析,首先要提取能够表示该语音的特征参数,有了特征参数才可能利用这些参数进行有效的处理,在对语音信号处理的过程中,语音信号的质量不仅取决于处理方法,同时取决于时候选对了合适的特征参数。
语音信号是一个非平稳的时变信号,但语音信号是由声门的激励脉冲通过声道形成的,而声道(人的口腔、鼻腔)的肌肉运动是缓慢的,所以“短时间”(10~30ms)内可以认为语音信号是平稳时不变的。由此构成了语音信号的“短时分析技术”。
在短时分析中,将语音信号分为一段一段的语音帧,每一帧一般取10~30ms,我们的研究就建立在每一帧的语音特征分析上。
提取的不同的语音特征参数对应着不同的语音信号分析方法:时域分析、频域分析、倒谱域分析...由于语音信号最重要的感知特性反映在功率谱上,而相位变化只起到很小的作用,所有语音频域分析更加重要。
信号加窗
1、矩形窗
$$w(n)=\left\{\begin{matrix} 1&&0\leq n\leq L-1\\ 0&&其他 \end{matrix}\right.$$
2、汉明窗(Hamming)
$$w(n)=\left\{\begin{matrix} \frac{1}{2}(1-cos(\frac{2\pi n}{L-1}))&&0\leq n\leq L-1\\ 0&&其他 \end{matrix}\right.$$
3、海宁窗(Hanning)
$$w(n)=\left\{\begin{matrix} 0.54-0.46cos(\frac{2\pi n}{L-1})&&0\leq n\leq L-1\\ 0&&其他 \end{matrix}\right.$$
通常对信号截断、分帧需要加窗,因为截断都有频域能量泄露,而窗函数可以减少截断带来的影响。
窗函数在scipy.signal信号处理工具箱中,如hanning窗:
import matplotlib.pyplot as pltimport scipy.signal as signalplt.figure(figsize=(6,2))plt.plot(signal.hanning(512))plt.show()
信号分帧
在分帧中,相邻两帧之间会有一部分重叠,帧长(wlen) = 重叠(overlap)+帧移(inc),如果相邻两帧之间不重叠,那么由于窗函数的形状,截取到的语音帧边缘会出现损失,所以要设置重叠部分。inc为帧移,表示后一帧第前一帧的偏移量,fs表示采样率,fn表示一段语音信号的分帧数。
$$\frac{N-overlap}{inc}=\frac{N-wlen+inc}{inc}$$
信号分帧的理论依据,其中x是语音信号,w是窗函数:
加窗截断类似采样,为了保证相邻帧不至于差别过大,通常帧与帧之间有帧移,其实就是插值平滑的作用。
给出示意图:
这里主要用到numpy工具包,涉及的指令有:
- np.repeat:主要是直接重复
- np.tile:主要是周期性重复
对比一下:
向量情况:
矩阵情况:
对于数据:
repeat操作:
tile操作:
对应结果:
对应分帧的代码实现:
这是没有加窗的示例:
import numpy as npimport waveimport os#import math def enframe(signal, nw, inc): '''将音频信号转化为帧。 参数含义: signal:原始音频型号 nw:每一帧的长度(这里指采样点的长度,即采样频率乘以时间间隔) inc:相邻帧的间隔(同上定义) ''' signal_length=len(signal) #信号总长度 if signal_length<=nw: #若信号长度小于一个帧的长度,则帧数定义为1 nf=1 else: #否则,计算帧的总长度 nf=int(np.ceil((1.0*signal_length-nw+inc)/inc)) pad_length=int((nf-1)*inc+nw) #所有帧加起来总的铺平后的长度 zeros=np.zeros((pad_length-signal_length,)) #不够的长度使用0填补,类似于FFT中的扩充数组操作 pad_signal=np.concatenate((signal,zeros)) #填补后的信号记为pad_signal indices=np.tile(np.arange(0,nw),(nf,1))+np.tile(np.arange(0,nf*inc,inc),(nw,1)).T #相当于对所有帧的时间点进行抽取,得到nf*nw长度的矩阵 indices=np.array(indices,dtype=np.int32) #将indices转化为矩阵 frames=pad_signal[indices] #得到帧信号# win=np.tile(winfunc(nw),(nf,1)) #window窗函数,这里默认取1# return frames*win #返回帧信号矩阵 return framesdef wavread(filename): f = wave.open(filename,'rb') params = f.getparams() nchannels, sampwidth, framerate, nframes = params[:4] strData = f.readframes(nframes)#读取音频,字符串格式 waveData = np.fromstring(strData,dtype=np.int16)#将字符串转化为int f.close() waveData = waveData*1.0/(max(abs(waveData)))#wave幅值归一化 waveData = np.reshape(waveData,[nframes,nchannels]).T return waveData filepath = "./data/" #添加路径dirname= os.listdir(filepath) #得到文件夹下的所有文件名称 filename = filepath+dirname[0]data = wavread(filename)nw = 512inc = 128Frame = enframe(data[0], nw, inc)
def enframe(signal, nw, inc, winfunc): '''将音频信号转化为帧。 参数含义: signal:原始音频型号 nw:每一帧的长度(这里指采样点的长度,即采样频率乘以时间间隔) inc:相邻帧的间隔(同上定义) ''' signal_length=len(signal) #信号总长度 if signal_length<=nw: #若信号长度小于一个帧的长度,则帧数定义为1 nf=1 else: #否则,计算帧的总长度 nf=int(np.ceil((1.0*signal_length-nw+inc)/inc)) pad_length=int((nf-1)*inc+nw) #所有帧加起来总的铺平后的长度 zeros=np.zeros((pad_length-signal_length,)) #不够的长度使用0填补,类似于FFT中的扩充数组操作 pad_signal=np.concatenate((signal,zeros)) #填补后的信号记为pad_signal indices=np.tile(np.arange(0,nw),(nf,1))+np.tile(np.arange(0,nf*inc,inc),(nw,1)).T #相当于对所有帧的时间点进行抽取,得到nf*nw长度的矩阵 indices=np.array(indices,dtype=np.int32) #将indices转化为矩阵 frames=pad_signal[indices] #得到帧信号 win=np.tile(winfunc,(nf,1)) #window窗函数,这里默认取1 return frames*win #返回帧信号矩阵
语音信号的短时时域处理
短时能量和短时平均幅度
短时能量和短时平均幅度的主要用途:
- 区分浊音和清音段,因为浊音的短时能量$E(i)$比清音大很多;
- 区分声母和韵母的分界和无话段和有话段的分界
短时平均过零率
对于连续语音信号,过零率意味着时域波形通过时间轴,对于离散信号,如果相邻的取样值改变符号,则称为过零。
作用:
发浊音时由于声门波引起谱的高频跌落,所以语音信号能量约集中在3kHz以下
发清音时多数能量集中在较高的频率上,
因为高频意味着高的短时平均过零率,低频意味着低的短时平均过零率,所以浊音时具有较低的过零率,而清音时具有较高的过零率。
利用短时平均过零率可以从背景噪声中找出语音信号,
2、可以用于判断寂静无话段与有话段的起点和终止位置。
3、在背景噪声较小的时候,用平均能量识别较为有效,在背景噪声较大的时候,用短时平均过零率识别较为有效。
短时自相关函数
短时自相关函数主要应用于端点检测和基音的提取,在韵母基因频率整数倍处将出现峰值特性,通常根据除R(0)外的第一峰值来估计基音,而在声母的短时自相关函数中看不到明显的峰值。
短时平均幅度差函数
用于检测基音周期,而且在计算上比短时自相关函数更加简单。
语音信号的短时频域处理
在语音信号处理中,在语音信号处理中,信号在频域或其他变换域上的分析处理占重要的位置,在频域上研究语音可以使信号在时域上无法表现出来的某些特征变得十分明显,一个音频信号的本质是由其频率内容决定的,
将时域信号转换为频域信号一般对语音进行短时傅里叶变换。
fft_audio = np.fft.fft(audio)
import numpy as npfrom scipy.io import wavfileimport matplotlib.pyplot as pltsampling_freq, audio = wavfile.read(r"C:\Windows\media\Windows Background.wav") # 读取文件audio = audio / np.max(audio) # 归一化,标准化# 应用傅里叶变换fft_signal = np.fft.fft(audio)print(fft_signal)# [-0.04022912+0.j -0.04068997-0.00052721j -0.03933007-0.00448355j# ... -0.03947908+0.00298096j -0.03933007+0.00448355j -0.04068997+0.00052721j]fft_signal = abs(fft_signal)print(fft_signal)# [0.04022912 0.04069339 0.0395848 ... 0.08001755 0.09203427 0.12889393]# 建立时间轴Freq = np.arange(0, len(fft_signal))# 绘制语音信号的plt.figure()plt.plot(Freq, fft_signal, color='blue')plt.xlabel('Freq (in kHz)')plt.ylabel('Amplitude')plt.show()
提取频域特征
将信号转换为频域之后,还需要将其转换为有用的形式,梅尔频率倒谱系数(MFCC),MFCC首先计算信号的功率谱,然后用滤波器组和离散余弦变换的组合来提取特征。
import numpy as npimport matplotlib.pyplot as pltfrom scipy.io import wavfilefrom python_speech_features import mfcc, logfbank# 读取输入音频文件sampling_freq, audio = wavfile.read("input_freq.wav")# 提取MFCC和滤波器组特征mfcc_features = mfcc(audio, sampling_freq)filterbank_features = logfbank(audio, sampling_freq)print('\nMFCC:\n窗口数 =', mfcc_features.shape[0])print('每个特征的长度 =', mfcc_features.shape[1])print('\nFilter bank:\n窗口数 =', filterbank_features.shape[0])print('每个特征的长度 =', filterbank_features.shape[1])# 画出特征图,将MFCC可视化。转置矩阵,使得时域是水平的mfcc_features = mfcc_features.Tplt.matshow(mfcc_features)plt.title('MFCC')# 将滤波器组特征可视化。转置矩阵,使得时域是水平的filterbank_features = filterbank_features.Tplt.matshow(filterbank_features)plt.title('Filter bank')plt.show()
语谱图
绝大部分信号都可以分解为若干不同频率的正弦波。这些正弦波中,频率最低的称为信号的基波,其余称为信号的谐波。基波只有一个,可以称为一次谐波,谐波可以有很多个,每次谐波的频率是基波频率的整数倍。谐波的大小可能互不相同。以谐波的频率为横坐标,幅值(大小)为纵坐标,绘制的系列条形图,称为频谱。频谱能够准确反映信号的内部构造。
语谱图综合了时域和频域的特点,明显的显示出来了语音频率随时间的变化情况,语谱图的横轴为时间,纵轴为频率任意给定频率成分在给定时刻的强弱用颜色深浅表示。颜色深表示频谱值大,颜色浅表示频谱值小,语谱图上不同的黑白程度形成不同的纹路,称为声纹,不用讲话者的声纹是不一样的,可以用做声纹识别。
其实得到了分帧信号,频域变换取幅值,就可以得到语谱图,如果仅仅是观察,matplotlib.pyplot有specgram指令:
import waveimport matplotlib.pyplot as pltimport numpy as npf = wave.open(r"C:\Windows\media\Windows Background.wav", "rb")params = f.getparams()nchannels, sampwidth, framerate, nframes = params[:4]strData = f.readframes(nframes)#读取音频,字符串格式waveData = np.fromstring(strData,dtype=np.int16)#将字符串转化为intwaveData = waveData*1.0/(max(abs(waveData)))#wave幅值归一化waveData = np.reshape(waveData,[nframes,nchannels]).Tf.close()plt.specgram(waveData[0],Fs = framerate, scale_by_freq = True, sides = 'default')plt.ylabel('Frequency(Hz)')plt.xlabel('Time(s)')plt.show()
[Y,FS]=audioread('p225_355_wb.wav');% specgram(Y,2048,44100,2048,1536); %Y1为波形数据 %FFT帧长2048点(在44100Hz频率时约为46ms) %采样频率44.1KHz %加窗长度,一般与帧长相等 %帧重叠长度,此处取为帧长的3/4specgram(Y,2048,FS,2048,1536);xlabel('时间(s)')ylabel('频率(Hz)')title('“概率”语谱图')
语音识别
import osimport argparseimport numpy as npfrom scipy.io import wavfilefrom hmmlearn import hmmfrom python_speech_features import mfcc# 解析命令行的输入参数def build_arg_parser(): parser = argparse.ArgumentParser(description='Trains the HMM classifier') parser.add_argument("--input-folder", dest="input_folder", required=True, help="Input folder containing the audio files in subfolders") return parser# 创建类,处理HMM相关过程class HMMTrainer(object): '''用到高斯隐马尔科夫模型 n_components:定义了隐藏状态的个数 cov_type:定义了转移矩阵的协方差类型 n_iter:定义了训练的迭代次数 ''' def __init__(self, model_name='GaussianHMM', n_components=4, cov_type='diag', n_iter=1000): self.model_name = model_name self.n_components = n_components self.cov_type = cov_type self.n_iter = n_iter self.models = [] if self.model_name == 'GaussianHMM': self.model = hmm.GaussianHMM(n_components=self.n_components, covariance_type=self.cov_type, n_iter=self.n_iter) else: raise TypeError('Invalid model type') # X是二维数组,其中每一行有13个数 def train(self, X): np.seterr(all='ignore') self.models.append(self.model.fit(X)) # 对输入数据运行模型 def get_score(self, input_data): return self.model.score(input_data)if __name__=='__main__': # 解析输入参数 args = build_arg_parser().parse_args() input_folder = args.input_folder hmm_models = [] # 初始化隐马尔科夫模型的变量 # 解析输入路径 for dirname in os.listdir(input_folder): # 获取子文件夹名称 subfolder = os.path.join(input_folder, dirname) if not os.path.isdir(subfolder): continue # 子文件夹名称即为该类的标记 # 提取特征 label = subfolder[subfolder.rfind('/') + 1:] # 初始化变量 X = np.array([]) y_words = [] # 迭代所有音频文件(分别保留一个进行测试) for filename in [x for x in os.listdir(subfolder) if x.endswith('.wav')][:-1]: # 读取每个音频文件 filepath = os.path.join(subfolder, filename) sampling_freq, audio = wavfile.read(filepath) # 提取MFCC特征 mfcc_features = mfcc(audio, sampling_freq) # 将MFCC特征添加到X变量 if len(X) == 0: X = mfcc_features else: X = np.append(X, mfcc_features, axis=0) # 添加标记 y_words.append(label) print('X.shape =', X.shape) # 训练并且保存HMM模型 hmm_trainer = HMMTrainer() hmm_trainer.train(X) hmm_models.append((hmm_trainer, label)) hmm_trainer = None # 测试文件 input_files = [ 'data/pineapple/pineapple15.wav', 'data/orange/orange15.wav', 'data/apple/apple15.wav', 'data/kiwi/kiwi15.wav' ] # 为输入数据分类 for input_file in input_files: # 读取每个音频文件 sampling_freq, audio = wavfile.read(input_file) # 提取MFCC特征 mfcc_features = mfcc(audio, sampling_freq) # 定义变量 max_score = None output_label = None # 迭代HMM模型并选取得分最高的模型 for item in hmm_models: hmm_model, label = item score = hmm_model.get_score(mfcc_features) if score > max_score: max_score = score output_label = label # 打印结果 print("\nTrue:", input_file[input_file.find('/')+1:input_file.rfind('/')]) print("Predicted:", output_label)
最后一个代码没有跑通,有很多原因,skearn-learn的库中minture库改动了。我看考下面的,但是还是跑不起来,应该就差一点点,后续我会把代码再调试。语音数据集在。
参考文献:
网址:用python做科学计算
python标准库wave模块
《python机器学习经典案例》美Prateek Joshi著
傅里叶变换的介绍:
各种音阶及其对应的频率
这篇博客的代码