在本章我們將學習如何讀寫WAV文件,如何利用聲卡實時地進行聲音的輸入輸出。標準的Python已經支持WAV文件的讀寫,而實時的聲音輸入輸出需要安裝pyAudio(http://people.csail.mit.edu/hubert/pyaudio)。最后我們還將看看如何使用pyMedia(http://pymedia.org)進行Mp3的解碼和播放。
掌握了上面的基礎知識之后,就可以做許多有趣的聲效處理的算法實驗了。聲效處理方面的內容將在以后的章節詳細介紹。
WAV是Microsoft開發的一種聲音文件格式,雖然它支持多種壓縮格式,不過它通常被用來保存未壓縮的聲音數據(PCM脈沖編碼調制)。WAV有三個重要的參數:聲道數、取樣頻率和量化位數。
例如CD中所儲存的聲音信號是雙聲道、44.1kHz、16bit。
如果你需要自己錄制和編輯聲音文件,推薦使用Audacity(http://audacity.sourceforge.net),它是一款開源的、跨平臺、多聲道的錄音編輯軟件。在我的工作中經常使用Audacity進行聲音信號的錄制,然后再輸出成WAV文件供Python程序處理。
下面讓我們來看看如何在Python中讀寫聲音文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | # -*- coding: utf-8 -*-
import wave
import pylab as pl
import numpy as np
# 打開WAV文檔
f = wave.open(r"c:\WINDOWS\Media\ding.wav", "rb")
# 讀取格式信息
# (nchannels, sampwidth, framerate, nframes, comptype, compname)
params = f.getparams()
nchannels, sampwidth, framerate, nframes = params[:4]
# 讀取波形數據
str_data = f.readframes(nframes)
f.close()
#將波形數據轉換為數組
wave_data = np.fromstring(str_data, dtype=np.short)
wave_data.shape = -1, 2
wave_data = wave_data.T
time = np.arange(0, nframes) * (1.0 / framerate)
# 繪制波形
pl.subplot(211)
pl.plot(time, wave_data[0])
pl.subplot(212)
pl.plot(time, wave_data[1], c="g")
pl.xlabel("time (seconds)")
pl.show()
|
WindowsXP的經典"叮"聲的波形
首先載入Python的標準處理WAV文件的模塊,然后調用wave.open打開wav文件,注意需要使用"rb"(二進制模式)打開文件:
import wave
f = wave.open(r"c:\WINDOWS\Media\ding.wav", "rb")
open返回一個的是一個Wave_read類的實例,通過調用它的方法讀取WAV文件的格式和數據:
getparams:一次性返回所有的WAV文件的格式信息,它返回的是一個組元(tuple):聲道數, 量化位數(byte單位), 采樣頻率, 采樣點數, 壓縮類型, 壓縮類型的描述。wave模塊只支持非壓縮的數據,因此可以忽略最后兩個信息:
params = f.getparams()
nchannels, sampwidth, framerate, nframes = params[:4]
getnchannels, getsampwidth, getframerate, getnframes等方法可以單獨返回WAV文件的特定的信息。
readframes:讀取聲音數據,傳遞一個參數指定需要讀取的長度(以取樣點為單位),readframes返回的是二進制數據(一大堆bytes),在Python中用字符串表示二進制數據:
str_data = f.readframes(nframes)
接下來需要根據聲道數和量化單位,將讀取的二進制數據轉換為一個可以計算的數組:
wave_data = np.fromstring(str_data, dtype=np.short)
通過fromstring函數將字符串轉換為數組,通過其參數dtype指定轉換后的數據格式,由于我們的聲音格式是以兩個字節表示一個取樣值,因此采用short數據類型轉換。現在我們得到的wave_data是一個一維的short類型的數組,但是因為我們的聲音文件是雙聲道的,因此它由左右兩個聲道的取樣交替構成:LRLRLRLR....LR(L表示左聲道的取樣值,R表示右聲道取樣值)。修改wave_data的sharp之后:
wave_data.shape = -1, 2
將其轉置得到:
wave_data = wave_data.T
整個轉換過程如下圖所示:
最后通過取樣點數和取樣頻率計算出每個取樣的時間:
time = np.arange(0, nframes) * (1.0 / framerate)
寫WAV文件的方法和讀類似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # -*- coding: utf-8 -*-
import wave
import numpy as np
import scipy.signal as signal
framerate = 44100
time = 10
# 產生10秒44.1kHz的100Hz - 1kHz的頻率掃描波
t = np.arange(0, time, 1.0/framerate)
wave_data = signal.chirp(t, 100, time, 1000, method='linear') * 10000
wave_data = wave_data.astype(np.short)
# 打開WAV文檔
f = wave.open(r"sweep.wav", "wb")
# 配置聲道數、量化位數和取樣頻率
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(framerate)
# 將wav_data轉換為二進制數據寫入文件
f.writeframes(wave_data.tostring())
f.close()
|
10-12行通過調用scipy.signal庫中的chrip函數,產生長度為10秒、取樣頻率為44.1kHz、100Hz到1kHz的頻率掃描波。由于chrip函數返回的數組為float64型,需要調用數組的astype方法將其轉換為short型。
18-20行分別設置輸出WAV文件的聲道數、量化位數和取樣頻率,當然也可以調用文件對象的setparams方法一次性配置所有的參數。最后21行調用文件的writeframes方法,將數組的內部的二進制數據寫入文件。writeframes方法會自動的更新WAV文件頭中的長度信息(nframes),保證其和真正的數據數量一致。
通過上一節介紹的讀寫聲音文件的方法,我們可以離線處理已經錄制好的聲音。不過更酷的是我們可以通過pyAudio庫從聲卡讀取聲音數據,處理之后再寫回聲卡,這樣就可以在電腦上實時地輸入、處理和輸出聲音數據。想象一下,我們可以做一個小程序,讀取麥克風的數據;加上回聲并和WAV文件中的數據進行混合;最后從聲卡輸出。這不就是一個Karaoke的原型么。
pyAudio是開源聲音庫PortAudio( http://www.portaudio.com )的Python綁定,目前它只支持阻塞式的輸入輸出模式。所謂阻塞式就是需要用戶的程序主動地去讀寫輸入輸出流。雖然阻塞式在功能上有所局限,但是由于編程比較簡單,非常適合一些處理聲音的腳本程序開發。
下面先來看看如何用pyAudio播放聲音。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # -*- coding: utf-8 -*-
import pyaudio
import wave
chunk = 1024
wf = wave.open(r"c:\WINDOWS\Media\ding.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.close()
p.terminate()
|
這段程序首先根據WAV文件的量化格式、聲道數和取樣頻率,分別配置open函數的各個參數,然后循環從WAV文件讀取數據,寫入用open函數打開的聲音輸出流。我們看到17-20行的while循環沒有任何等待的代碼。因為pyAudio使用阻塞模式,因此當底層的輸出數據緩存沒有空間保存數據時,stream.write會阻塞用戶程序,直到stream.write能將數據寫入輸出緩存。
PyAudio類的open函數有許多參數:
從聲卡讀取數據和寫入數據一樣簡單,下面我們用一個簡單的聲音監測小程序來展示一下如何用pyAudio讀取聲音數據。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | # -*- coding: utf-8 -*-
from pyaudio import PyAudio, paInt16
import numpy as np
from datetime import datetime
import 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("".join(data))
wf.close()
NUM_SAMPLES = 2000 # pyAudio內部緩存的塊的大小
SAMPLING_RATE = 8000 # 取樣頻率
LEVEL = 1500 # 聲音保存的閾值
COUNT_NUM = 20 # NUM_SAMPLES個取樣之內出現COUNT_NUM個大于LEVEL的取樣則記錄聲音
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 = datetime.now().strftime("%Y-%m-%d_%H_%M_%S") + ".wav"
save_wave_file(filename, save_buffer)
save_buffer = []
print filename, "saved"
|
此程序一開頭是一系列的全局變量,用來配置錄音的一些參數:以SAMPLING_RATE為采樣頻率,每次讀入一塊有NUM_SAMPLES個采樣的數據塊,當讀入的采樣數據中有COUNT_NUM個值大于LEVEL的取樣的時候,將數據保存進WAV文件,一旦開始保存數據,所保存的數據長度最短為SAVE_LENGTH個塊。WAV文件以保存時的時刻作為文件名。
從聲卡讀入的數據和從WAV文件讀入的類似,都是二進制數據,由于我們用paInt16格式(16bit的short類型)保存采樣值,因此將它自己轉換為dtype為np.short的數組。