音频显示是 pzh-py-speech 的主要功能,pzh-py-speech 借助的是 Matplotlib 以及 NumPy 来实现的音频显示功能,今天痞子衡为大家介绍音频显示在 pzh-py-speech 中是如何实现的。

 

一、SciPy 工具集

SciPy 是一套 Python 科学计算相关的工具集,其本身也是一个 Python 库,这个工具集主要包含以下 6 大 Python 库,pzh-py-speech 所用到的 Matplotlib 以及 NumPy 均属于 SciPy 工具集。

 

 

1.1 NumPy

NumPy 是一套最基础的 Python 科学计算包,它主要用于数组与矩阵运算,它是一个开源项目,被收录进 NumFOCUS 组织维护的 Sponsored Project 里。pzh-py-speech 使用的是 NumPy 1.15.0。
  

NumPy 库的官方主页如下:

 

NumPy 官方主页: /zixunimg/eefocusimg/www.numpy.org/


NumPy 安装方法: /zixunimg/eefocusimg/pypi.org/project/numpy/
  

NumPy 的快速上手可参考这个网页 /zixunimg/eefocusimg/docs.scipy.org/doc/numpy/user/quickstart.html

 

1.2 Matplotlib

Matplotlib 是一套 Python 高质量 2D 绘图库,它的初始设计者为 John Hunter,它也是一个开源项目,被同样收录进 NumFOCUS 组织维护的 Sponsored Project 里。pzh-py-speech 使用的是 Matplotlib 2.2.3。
  

Matplotlib 库的官方主页如下:

 

Matplotlib 官方主页: /zixunimg/eefocusimg/matplotlib.org/


Matplotlib 安装方法: /zixunimg/eefocusimg/pypi.org/project/matplotlib/
  

Matplotlib 绘图功能非常强大,但是作为一般使用,我们没有必要去通读其官方文档,其提供了非常多的 example 代码,这些 example 都在 /zixunimg/eefocusimg/matplotlib.org/gallery/index.html, 我们只要找到能满足我们需求的 example,在其基础上简单修改即可。下面就是一个最简单的正弦波示例:

 

 

import matplotlib


import matplotlib.pyplot as plt


import numpy as np

 

# Data for plotting


t = np.arange(0.0, 2.0, 0.01)


s = 1 + np.sin(2 * np.pi * t)

 

fig, ax = plt.subplots()


ax.plot(t, s)

 

ax.set(xlabel='time (s)', ylabel='voltage (mV)',


title='About as simple as it gets, folks')


ax.grid()

 

fig.savefig("test.png")


plt.show()


二、pzh-py-speech 音频显示实现

pzh-py-speech 关于音频显示功能实现主要有四点:选择 .wav 文件、读取 .wav 文件、绘制 .wav 波形、添加光标功能,最终 pzh-py-speech 效果如下图所示,痞子衡为逐一为大家介绍实现细节。

 

 

2.1 选择 .wav 文件功能

选择 wav 文件主要借助的是 wxPython 里的 genericDirCtrl 控件提供的功能实现的,我们使用 genericDirCtrl 控件创建了一个名为 m_genericDirCtrl_audioDir 的对象,借助其 SetFilter()方法实现了仅显示 .wav 文件格式的过滤,并且我们为 m_genericDirCtrl_audioDir 还创建了一个 event,即 viewAudio(),这个 event 的触发条件是选中 m_genericDirCtrl_audioDir 里列出的 .wav 文件,当 viewAudio()被触发时,我们通过 GetFilePath()方法即可获得选中的 .wav 文件路径。

 

class mainWin(win.speech_win):

   

def __init__(self, parent):
       

win.speech_win.__init__(self, parent)
       

# ...
       

self.m_genericDirCtrl_audioDir.SetFilter("Audio files (*.wav)|*.wav")

   

def viewAudio( self, event ):
       

self.wavPath =  self.m_genericDirCtrl_audioDir.GetFilePath()

 

2.2 读取 .wav 文件功能

读取 .wav 文件主要借助的是 python 自带的标准库 wave,以及第三方的 NumPy 库。痞子衡创建了一个名为 wavCanvasPanel 的类,在这个类中定义了 readWave(self, wavPath, wavInfo)方法,其中参数 wavPath 即是要读取的 .wav 文件路径,参数 wavInfo 是 GUI 状态栏对象,用于直观显示读取到的 .wav 文件信息。
  

在 wavCanvasPanel.readWave()方法中,痞子衡首先使用了 wave 库里的功能获取到 .wav 文件的所有信息以及所有 PCM 数据,然后借助 NumPy 库将 PCM 数据按 channel 重新组织,便于后续图形显示。关于数据重新组织,有一个地方需要特别说明,即 int24 类型(3-byte)是不被 NumPy 中的 fromstring()原生支持,因此痞子衡自己实现了一个非标准类型数据的 fromstring()。

 

import numpy


import wave

 

class wavCanvasPanel(wx.Panel):

   

def fromstring(self, wavData, alignedByte):
       

if alignedByte <= 8:
           

src = numpy.ndarray(len(wavData), numpy.dtype('>i1'), wavData)
           

dest = numpy.zeros(len(wavData) / alignedByte, numpy.dtype('>i8'))
           

for i in range(alignedByte):
               

dest.view(dtype='>i1')[alignedByte-1-i::8] = src.view(dtype='>i1')[i::alignedByte]
           

[hex(x) for x in dest]
           

return True, dest
       

else:
           

return False, wavData

   

def readWave(self, wavPath, wavInfo):
       

if os.path.isfile(wavPath):
           

# Open the wav file to get wave data and parameters
           

wavFile =  wave.open(wavPath, "rb")
           

wavParams = wavFile.getparams()
           

wavChannels = wavParams[0]
           

wavSampwidth = wavParams[1]
           

wavFramerate = wavParams[2]
           

wavFrames = wavParams[3]
           

wavInfo.SetStatusText('Opened Audio Info = ' +
                                 

'Channels:' + str(wavChannels) +
                                 

', SampWidth:' + str(wavSampwidth) + 'Byte' +
                                 

', SampRate:' + str(wavFramerate) + 'kHz' +
                                 

', FormatTag:' + wavParams[4])
           

wavData = wavFile.readframes(wavFrames)
           

wavFile.close()
           

# Transpose the wav data if wave has multiple channels
           

if wavSampwidth == 1:
               

dtype = numpy.int8
           

elif wavSampwidth == 2:
               

dtype = numpy.int16
           

elif wavSampwidth == 3:
               

dtype = None
           

elif wavSampwidth == 4:
               

dtype = numpy.float32
           

else:
               

return 0, 0, 0
           

if dtype != None:
               

retData = numpy.fromstring(wavData, dtype = dtype)
           

else:
               

# Implement int24 manually
               

status, retData = self.fromstring(wavData, 3)
               

if not status:
                   

return 0, 0, 0
           

if wavChannels != 1:
               

retData.shape = -1, wavChannels
               

retData = retData.T
           

# Calculate and arange wave time
           

retTime = numpy.arange(0, wavFrames) * (1.0 / wavFramerate)
           

retChannels = wavChannels
           

return retChannels, retData, retTime
       

else:
           

return 0, 0, 0

 

2.3 绘制 .wav 波形功能

绘制 .wav 波形是最主要的功能。痞子衡在 wavCanvasPanel 类中实现了 showWave(self, wavPath, wavInfo)方法,这个方法会在 GUI 控件 m_genericDirCtrl_audioDir 的事件函数 viewAudio()中被调用。
  

在 wavCanvasPanel.showWave()方法中,痞子衡首先使用了 readWave()获取 .wav 文件中经过重新组织的 PCM 数据,然后借助 Matplotlib 中的 figure 类中的 add_axes()方法逐一将各 channel 的 PCM 数据绘制出来,并辅以各种信息(x、y 轴精度、标签等)一同显示出来。由于 GUI 控件里专门用于显示波形的 Panel 对象尺寸为 720*360 inch,痞子衡限制了最多显示 .wav 的前 8 通道。

 

import matplotlib


from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas


from matplotlib.figure import Figure

 

MAX_AUDIO_CHANNEL = 8


#unit: inch


PLOT_PANEL_WIDTH = 720


PLOT_PANEL_HEIGHT = 360


#unit: percent


PLOT_AXES_WIDTH_TITLE = 0.05


PLOT_AXES_HEIGHT_LABEL = 0.075

 

class wavCanvasPanel(wx.Panel):

   

def __init__(self, parent):
       

wx.Panel.__init__(self, parent)
       

dpi = 60
       

width = PLOT_PANEL_WIDTH / dpi
       

height = PLOT_PANEL_HEIGHT / dpi
       

self.wavFigure = Figure(figsize=[width,height], dpi=dpi, facecolor='#404040')
       

self.wavCanvas = FigureCanvas(self, -1, self.wavFigure)
       

self.wavSizer = wx.BoxSizer(wx.VERTICAL)
       

self.wavSizer.Add(self.wavCanvas, 1, wx.EXPAND|wx.ALL)
       

self.SetSizerAndFit(self.wavSizer)
       

self.wavAxes = [None] * MAX_AUDIO_CHANNEL

   

def readWave(self, wavPath, wavInfo):
       

# ...

   

def showWave(self, wavPath, wavInfo):
       

self.wavFigure.clear()
       

waveChannels, waveData, waveTime = self.readWave(wavPath, wavInfo)
       

if waveChannels != 0:
           

# Note: only show max supported channel if actual channel > max supported channel
           

if waveChannels > MAX_AUDIO_CHANNEL:
               

waveChannels = MAX_AUDIO_CHANNEL
           

# Polt the waveform of each channel in sequence
           

for i in range(waveChannels):
               

left = PLOT_AXES_HEIGHT_LABEL
               

bottom = (1.0 / waveChannels) * (waveChannels - 1 - i) + PLOT_AXES_HEIGHT_LABEL
               

height = 1.0 / waveChannels - (PLOT_AXES_WIDTH_TITLE + PLOT_AXES_HEIGHT_LABEL)
               

width = 1 - left - 0.05
               

self.wavAxes[i] = self.wavFigure.add_axes([left, bottom, width, height], facecolor='k')
               

self.wavAxes[i].set_prop_cycle(color='#00F279', lw=[1])
               

self.wavAxes[i].set_xlabel('time (s)', color='w')
               

self.wavAxes[i].set_ylabel('value', color='w')
               

if waveChannels == 1:
                   

data = waveData
               

else:
                   

data = waveData[i]
               

self.wavAxes[i].plot(waveTime, data)
               

self.wavAxes[i].grid()
               

self.wavAxes[i].tick_params(labelcolor='w')
               

self.wavAxes[i].set_title('Audio Channel ' + str(i), color='w')
       

# Note!!!: draw() must be called if figure has been cleared once
       

self.wavCanvas.draw()

 

class mainWin(win.speech_win):

   

def __init__(self, parent):
       

win.speech_win.__init__(self, parent)
       

self.wavPanel = wavCanvasPanel(self.m_panel_plot)
       

# ...

   

def viewAudio( self, event ):
       

self.wavPath =  self.m_genericDirCtrl_audioDir.GetFilePath()
       

self.wavPanel.showWave(self.wavPath, self.statusBar)

 

2.4 添加光标功能

光标定位功能不是必要功能,但其可以让软件看起来高大上,痞子衡创建了一个名为 wavCursor 类来实现它,主要在这个类中实现了 moveMouse 方法,这个方法将会被 FigureCanvasWxAgg 类中的 mpl_connect()方法添加到各通道 axes 中。

MAX_AUDIO_CHANNEL = 8

class wavCursor(object):
   

def __init__(self, ax, x, y):
       

self.ax = ax
       

self.vline = ax.axvline(color='r', alpha=1)
       

self.hline = ax.axhline(color='r', alpha=1)
       

self.marker, = ax.plot([0],[0], marker="o", color="crimson", zorder=3)
       

self.x = x
       

self.y = y
       

self.xlim = self.x[len(self.x)-1]
       

self.text = ax.text(0.7, 0.9, '', bbox=dict(facecolor='red', alpha=0.5))

   

def moveMouse(self, event):
       

if not event.inaxes:
           

return
       

x, y = event.xdata, event.ydata
       

if x > self.xlim:
           

x = self.xlim
       

index = numpy.searchsorted(self.x, [x])[0]
       

x = self.x[index]
       

y = self.y[index]
       

self.vline.set_xdata(x)
       

self.hline.set_ydata(y)
       

self.marker.set_data([x],[y])
       

self.text.set_text('x=%1.2f, y=%1.2f' % (x, y))
       

self.text.set_position((x,y))
       

self.ax.figure.canvas.draw_idle()

 

class wavCanvasPanel(wx.Panel):
   

def __init__(self, parent):
       

# ...
       

self.wavAxes = [None] * MAX_AUDIO_CHANNEL
       

# 定义光标对象
       

self.wavCursor = [None] * MAX_AUDIO_CHANNEL

   

def showWave(self, wavPath, wavInfo):
       

# ...
       

if waveChannels != 0:
           

# ...
           

for i in range(waveChannels):
               

# ...
               

self.wavAxes[i].set_title('Audio Channel ' + str(i), color='w')
               

# 实例化光标对象,并使用 mpl_connect()将 moveMouse()动作加入光标对象
               

self.wavCursor[i] = wavCursor(self.wavAxes[i], waveTime, data)
               

self.wavCanvas.mpl_connect('motion_notify_event', self.wavCursor[i].moveMouse)
       

# ...
  

至此,语音处理工具 pzh-py-speech 诞生之音频显示实现痞子衡便介绍完毕了,掌声在哪里~~~