1. 程式人生 > >痞子衡嵌入式:語音處理工具pzh-speech誕生記(4)- 音訊錄播實現(PyAudio)

痞子衡嵌入式:語音處理工具pzh-speech誕生記(4)- 音訊錄播實現(PyAudio)


  大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家介紹的是語音處理工具pzh-py-speech誕生之音訊錄播實現。

  音訊錄播是pzh-py-speech的主要功能,pzh-py-speech藉助的是Python自帶wave庫以及第三方PyAudio庫來實現的音訊播放和錄製功能,今天痞子衡為大家介紹音訊錄播在pzh-py-speech中是如何實現的。

一、wave簡介

  wave是python標準庫,其可以實現wav音訊檔案的讀寫,並且能解析wav音訊的引數。pzh-py-speech藉助wave庫來讀寫wav檔案,播放音訊時藉助wave庫來讀取wav檔案並獲取音訊引數(通道,取樣寬度,取樣率),錄製音訊時藉助wave庫來設定音訊引數並儲存成wav檔案。下面列舉了pzh-py-speech所用到的全部API:

  • wave用法: https://docs.python.org/2/library/wave.html
wave.open()

# wav音訊讀API
Wave_read.getnchannels()       # 獲取音訊通道數
Wave_read.getsampwidth()       # 獲取音訊取樣寬度
Wave_read.getframerate()       # 獲取音訊取樣率
Wave_read.getnframes()         # 獲取音訊總幀數
Wave_read.readframes(n)        # 讀取音訊幀資料
Wave_read.tell()               # 獲取已讀取的音訊幀數
Wave_read.close()

# wav音訊寫API
Wave_write.setnchannels(n)     # 設定音訊通道數
Wave_write.setsampwidth(n)     # 設定音訊取樣寬度
Wave_write.setframerate(n)     # 設定音訊取樣率
Wave_write.writeframes(data)   # 寫入音訊幀資料
Wave_write.close()

二、PyAudio簡介

  PyAudio是開源跨平臺音訊庫PortAudio的python封裝,PyAudio庫的維護者是Hubert Pham,該庫從2006年開始推出,一直持續更新至今,pzh-py-speech使用的是PyAudio 0.2.11。
  pzh-py-speech藉助PyAudio庫來實現音訊資料流控制(包括從PC麥克風獲取音訊流,將音訊流輸出給PC揚聲器),如果說wave庫實現的是對wav檔案的單純操作,那麼PyAudio庫則實現的是音訊相關硬體裝置的互動。
  PyAudio專案的官方主頁如下:

  • PortAudio官方主頁: http://www.portaudio.com/
  • PyAudio官方主頁: http://people.csail.mit.edu/hubert/pyaudio/
  • PyAudio安裝方法: https://pypi.org/project/PyAudio/

  PyAudio對音訊流的控制有兩種,一種是阻塞式的,另一種是非阻塞式的(callback),前者一般用於確定的音訊控制(比如單純播放一個本地音訊檔案,並且中途不會有暫停/繼續等操作),後者一般用於靈活的音訊控制(比如錄製一段音訊,但是要等待一個事件響應才會結束)。pzh-py-speech用的是後者。下面是兩種方式的音訊播放使用示例:

import pyaudio
import wave

CHUNK = 1024

wf = wave.open(“test.wav”, 'rb')
p = pyaudio.PyAudio()

##########################################################
# 此為阻塞式,迴圈讀取1024個byte音訊資料去播放,直到test.wav檔案資料被全部讀出
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                channels=wf.getnchannels(),
                rate=wf.getframerate(),
                output=True)
data = wf.readframes(CHUNK)
while data != '':
    stream.write(data)
    data = wf.readframes(CHUNK)
##########################################################
# 此為非阻塞式的(callback),系統會自動讀取test.wav檔案裡的音訊幀,直到播放完畢
def callback(in_data, frame_count, time_info, status):
    data = wf.readframes(frame_count)
    return (data, pyaudio.paContinue)
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                channels=wf.getnchannels(),
                rate=wf.getframerate(),
                output=True,
                stream_callback=callback)
stream.start_stream()
while stream.is_active():
    time.sleep(0.1)
##########################################################

stream.stop_stream()
stream.close()
p.terminate()

三、pzh-py-speech音訊錄播實現

3.1 播放實現

  播放功能本身實現不算複雜,但pzh-py-speech裡實現的是播放按鈕的五種狀態Start -> Play -> Pause -> Resume -> End控制,即播放中途實現了暫停和恢復,因此程式碼要稍微複雜一些。此處的重點是playAudioCallback()函式裡的else分支,如果在暫停狀態下,必須還是要給PyAudio返回一段空資料:

import wave
import pyaudio

AUDIO_PLAY_STATE_START = 0
AUDIO_PLAY_STATE_PLAY = 1
AUDIO_PLAY_STATE_PAUSE = 2
AUDIO_PLAY_STATE_RESUME = 3
AUDIO_PLAY_STATE_END = 4

class mainWin(win.speech_win):

    def __init__(self, parent):
        # ...
        # Start -> Play -> Pause -> Resume -> End
        self.playState = AUDIO_PLAY_STATE_START

    def viewAudio( self, event ):
        self.wavPath =  self.m_genericDirCtrl_audioDir.GetFilePath()
        if self.playState != AUDIO_PLAY_STATE_START:
            self.playState = AUDIO_PLAY_STATE_END
            self.m_button_play.SetLabel('Play Start')

    def playAudioCallback(self, in_data, frame_count, time_info, status):
        if self.playState == AUDIO_PLAY_STATE_PLAY or self.playState == AUDIO_PLAY_STATE_RESUME:
            data = self.wavFile.readframes(frame_count)
            if self.wavFile.getnframes() == self.wavFile.tell():
                status = pyaudio.paComplete
                self.playState = AUDIO_PLAY_STATE_END
                self.m_button_play.SetLabel('Play Start')
            else:
                status = pyaudio.paContinue
            return (data, status)
        else:
            # Note!!!:
            data = numpy.zeros(frame_count*self.wavFile.getnchannels()).tostring()
            return (data, pyaudio.paContinue)

    def playAudio( self, event ):
        if os.path.isfile(self.wavPath):
            if self.playState == AUDIO_PLAY_STATE_END:
                self.playState = AUDIO_PLAY_STATE_START
                self.wavStream.stop_stream()
                self.wavStream.close()
                self.wavPyaudio.terminate()
                self.wavFile.close()
            if self.playState == AUDIO_PLAY_STATE_START:
                self.playState = AUDIO_PLAY_STATE_PLAY
                self.m_button_play.SetLabel('Play Pause')
                self.wavFile =  wave.open(self.wavPath, "rb")
                self.wavPyaudio = pyaudio.PyAudio()
                self.wavStream = self.wavPyaudio.open(format=self.wavPyaudio.get_format_from_width(self.wavFile.getsampwidth()),
                                                      channels=self.wavFile.getnchannels(),
                                                      rate=self.wavFile.getframerate(),
                                                      output=True,
                                                      stream_callback=self.playAudioCallback)
                self.wavStream.start_stream()
            elif self.playState == AUDIO_PLAY_STATE_PLAY or self.playState == AUDIO_PLAY_STATE_RESUME:
                self.playState = AUDIO_PLAY_STATE_PAUSE
                self.m_button_play.SetLabel('Play Resume')
            elif self.playState == AUDIO_PLAY_STATE_PAUSE:
                self.playState = AUDIO_PLAY_STATE_RESUME
                self.m_button_play.SetLabel('Play Pause')
            else:
                pass

3.2 錄製實現

  相比播放功能,錄製功能就簡單了些,因為錄製按鈕狀態就兩種Start -> End,暫不支援中斷後繼續錄製。這裡的重點主要是音訊三大引數(取樣寬度,取樣率,通道數)設定的支援:

import wave
import pyaudio

class mainWin(win.speech_win):

    def recordAudioCallback(self, in_data, frame_count, time_info, status):
        if not self.isRecording:
            status = pyaudio.paComplete
        else:
            self.wavFrames.append(in_data)
            status = pyaudio.paContinue
        return (in_data, status)

    def recordAudio( self, event ):
        if not self.isRecording:
            self.isRecording = True
            self.m_button_record.SetLabel('Record Stop')
            # Get the wave parameter from user settings
            fileName = self.m_textCtrl_recFileName.GetLineText(0)
            if fileName == '':
                fileName = 'rec_untitled1.wav'
            self.wavPath = os.path.join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))), 'conv', 'rec', fileName)
            self.wavSampRate = int(self.m_choice_sampRate.GetString(self.m_choice_sampRate.GetSelection()))
            channels = self.m_choice_channels.GetString(self.m_choice_channels.GetSelection())
            if channels == 'Mono':
                self.wavChannels = 1
            else: #if channels == 'Stereo':
                self.wavChannels = 2
            bitDepth = int(self.m_choice_bitDepth.GetString(self.m_choice_bitDepth.GetSelection()))
            if bitDepth == 8:
                self.wavBitFormat = pyaudio.paInt8
            elif bitDepth == 24:
                self.wavBitFormat = pyaudio.paInt24
            elif bitDepth == 32:
                self.wavBitFormat = pyaudio.paFloat32
            else:
                self.wavBitFormat = pyaudio.paInt16
            # Record audio according to wave parameters
            self.wavFrames = []
            self.wavPyaudio = pyaudio.PyAudio()
            self.wavStream = self.wavPyaudio.open(format=self.wavBitFormat,
                                                  channels=self.wavChannels,
                                                  rate=self.wavSampRate,
                                                  input=True,
                                                  frames_per_buffer=AUDIO_CHUNK_SIZE,
                                                  stream_callback=self.recordAudioCallback)
            self.wavStream.start_stream()
        else:
            self.isRecording = False
            self.m_button_record.SetLabel('Record Start')
            self.wavStream.stop_stream()
            self.wavStream.close()
            self.wavPyaudio.terminate()
            # Save the wave data into file
            wavFile = wave.open(self.wavPath, 'wb')
            wavFile.setnchannels(self.wavChannels)
            wavFile.setsampwidth(self.wavPyaudio.get_sample_size(self.wavBitFormat))
            wavFile.setframerate(self.wavSampRate)
            wavFile.writeframes(b''.join(self.wavFrames))
            wavFile.close()

  至此,語音處理工具pzh-py-speech誕生之音訊錄播實現痞子衡便介紹完畢了,掌聲在哪裡~~~

參考文件

  1. Python解析Wav檔案並繪製波形的方法