加入星計(jì)劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴(kuò)散
  • 作品版權(quán)保護(hù)
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長期合作伙伴
立即加入
  • 正文
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請入駐 產(chǎn)業(yè)圖譜

400行Python代碼實(shí)現(xiàn)文語處理助手(3) - 音頻顯示

2020/02/10
101
閱讀需 29 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

音頻顯示是 pzh-py-speech 的主要功能,pzh-py-speech 借助的是 Matplotlib 以及 NumPy 來實(shí)現(xiàn)的音頻顯示功能,今天痞子衡為大家介紹音頻顯示在 pzh-py-speech 中是如何實(shí)現(xiàn)的。

一、SciPy 工具集

SciPy 是一套 Python 科學(xué)計(jì)算相關(guān)的工具集,其本身也是一個(gè) Python 庫,這個(gè)工具集主要包含以下 6 大 Python 庫,pzh-py-speech 所用到的 Matplotlib 以及 NumPy 均屬于 SciPy 工具集。

1.1 NumPy

NumPy 是一套最基礎(chǔ)的 Python 科學(xué)計(jì)算包,它主要用于數(shù)組與矩陣運(yùn)算,它是一個(gè)開源項(xiàng)目,被收錄進(jìn) NumFOCUS 組織維護(hù)的 Sponsored Project 里。pzh-py-speech 使用的是 NumPy 1.15.0。
  

NumPy 庫的官方主頁如下:

NumPy 官方主頁: http://www.numpy.org/

NumPy 安裝方法: https://pypi.org/project/numpy/
  

NumPy 的快速上手可參考這個(gè)網(wǎng)頁 https://docs.scipy.org/doc/numpy/user/quickstart.html

1.2 Matplotlib

Matplotlib 是一套 Python 高質(zhì)量 2D 繪圖庫,它的初始設(shè)計(jì)者為 John Hunter,它也是一個(gè)開源項(xiàng)目,被同樣收錄進(jìn) NumFOCUS 組織維護(hù)的 Sponsored Project 里。pzh-py-speech 使用的是 Matplotlib 2.2.3。
  

Matplotlib 庫的官方主頁如下:

Matplotlib 官方主頁: https://matplotlib.org/

Matplotlib 安裝方法: https://pypi.org/project/matplotlib/
  

Matplotlib 繪圖功能非常強(qiáng)大,但是作為一般使用,我們沒有必要去通讀其官方文檔,其提供了非常多的 example 代碼,這些 example 都在 https://matplotlib.org/gallery/index.html, 我們只要找到能滿足我們需求的 example,在其基礎(chǔ)上簡單修改即可。下面就是一個(gè)最簡單的正弦波示例:

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 音頻顯示實(shí)現(xiàn)

pzh-py-speech 關(guān)于音頻顯示功能實(shí)現(xiàn)主要有四點(diǎn):選擇 .wav 文件、讀取 .wav 文件、繪制 .wav 波形、添加光標(biāo)功能,最終 pzh-py-speech 效果如下圖所示,痞子衡為逐一為大家介紹實(shí)現(xiàn)細(xì)節(jié)。

2.1 選擇 .wav 文件功能

選擇 wav 文件主要借助的是 wxPython 里的 genericDirCtrl 控件提供的功能實(shí)現(xiàn)的,我們使用 genericDirCtrl 控件創(chuàng)建了一個(gè)名為 m_genericDirCtrl_audioDir 的對象,借助其 SetFilter()方法實(shí)現(xiàn)了僅顯示 .wav 文件格式的過濾,并且我們?yōu)?m_genericDirCtrl_audioDir 還創(chuàng)建了一個(gè) event,即 viewAudio(),這個(gè) event 的觸發(fā)條件是選中 m_genericDirCtrl_audioDir 里列出的 .wav 文件,當(dāng) viewAudio()被觸發(fā)時(shí),我們通過 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 自帶的標(biāo)準(zhǔn)庫 wave,以及第三方的 NumPy 庫。痞子衡創(chuàng)建了一個(gè)名為 wavCanvasPanel 的類,在這個(gè)類中定義了 readWave(self, wavPath, wavInfo)方法,其中參數(shù) wavPath 即是要讀取的 .wav 文件路徑,參數(shù) wavInfo 是 GUI 狀態(tài)欄對象,用于直觀顯示讀取到的 .wav 文件信息。
  

在 wavCanvasPanel.readWave()方法中,痞子衡首先使用了 wave 庫里的功能獲取到 .wav 文件的所有信息以及所有 PCM 數(shù)據(jù),然后借助 NumPy 庫將 PCM 數(shù)據(jù)按 channel 重新組織,便于后續(xù)圖形顯示。關(guān)于數(shù)據(jù)重新組織,有一個(gè)地方需要特別說明,即 int24 類型(3-byte)是不被 NumPy 中的 fromstring()原生支持,因此痞子衡自己實(shí)現(xiàn)了一個(gè)非標(biāo)準(zhǔn)類型數(shù)據(jù)的 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 類中實(shí)現(xiàn)了 showWave(self, wavPath, wavInfo)方法,這個(gè)方法會(huì)在 GUI 控件 m_genericDirCtrl_audioDir 的事件函數(shù) viewAudio()中被調(diào)用。
  

在 wavCanvasPanel.showWave()方法中,痞子衡首先使用了 readWave()獲取 .wav 文件中經(jīng)過重新組織的 PCM 數(shù)據(jù),然后借助 Matplotlib 中的 figure 類中的 add_axes()方法逐一將各 channel 的 PCM 數(shù)據(jù)繪制出來,并輔以各種信息(x、y 軸精度、標(biāo)簽等)一同顯示出來。由于 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 添加光標(biāo)功能

光標(biāo)定位功能不是必要功能,但其可以讓軟件看起來高大上,痞子衡創(chuàng)建了一個(gè)名為 wavCursor 類來實(shí)現(xiàn)它,主要在這個(gè)類中實(shí)現(xiàn)了 moveMouse 方法,這個(gè)方法將會(huì)被 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
? ? ? ?

# 定義光標(biāo)對象
? ? ? ?

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')
? ? ? ? ? ? ? ?

# 實(shí)例化光標(biāo)對象,并使用 mpl_connect()將 moveMouse()動(dòng)作加入光標(biāo)對象
? ? ? ? ? ? ? ?

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

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

# ...
  

至此,語音處理工具 pzh-py-speech 誕生之音頻顯示實(shí)現(xiàn)痞子衡便介紹完畢了,掌聲在哪里~~~

相關(guān)推薦

電子產(chǎn)業(yè)圖譜

碩士畢業(yè)于蘇州大學(xué)電子信息學(xué)院,目前就職于恩智浦(NXP)半導(dǎo)體MCU系統(tǒng)部門,擔(dān)任嵌入式系統(tǒng)應(yīng)用工程師。痞子衡會(huì)定期分享嵌入式相關(guān)文章