# 聲音的輸入輸出
在本章我們將學習如何讀寫WAV文件,如何利用聲卡實時地進行聲音的輸入輸出。標準的Python已經支持WAV文件的讀寫,而實時的聲音輸入輸出需要安裝pyAudio([http://people.csail.mit.edu/hubert/pyaudio](http://people.csail.mit.edu/hubert/pyaudio))。最后我們還將看看如何使用pyMedia([http://pymedia.org](http://pymedia.org))進行Mp3的解碼和播放。
掌握了上面的基礎知識之后,就可以做許多有趣的聲效處理的算法實驗了。聲效處理方面的內容將在以后的章節詳細介紹。
## 讀寫Wave文件
WAV是Microsoft開發的一種聲音文件格式,雖然它支持多種壓縮格式,不過它通常被用來保存未壓縮的聲音數據(PCM脈沖編碼調制)。WAV有三個重要的參數:聲道數、取樣頻率和量化位數。
* 聲道數:可以是單聲道或者是雙聲道
* 采樣頻率:一秒內對聲音信號的采集次數,常用的有8kHz, 16kHz, 32kHz, 48kHz, 11.025kHz, 22.05kHz, 44.1kHz
* 量化位數:用多少bit表達一次采樣所采集的數據,通常有8bit、16bit、24bit和32bit等幾種
例如CD中所儲存的聲音信號是雙聲道、44.1kHz、16bit。
如果你需要自己錄制和編輯聲音文件,推薦使用Audacity([http://audacity.sourceforge.net](http://audacity.sourceforge.net)),它是一款開源的、跨平臺、多聲道的錄音編輯軟件。在我的工作中經常使用Audacity進行聲音信號的錄制,然后再輸出成WAV文件供Python程序處理。
### 讀Wave文件
下面讓我們來看看如何在Python中讀寫聲音文件:
```
# -*- 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)
```
### 寫Wave文件
寫WAV文件的方法和讀類似:
```
# -*- 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播放和錄音
通過上一節介紹的讀寫聲音文件的方法,我們可以離線處理已經錄制好的聲音。不過更酷的是我們可以通過pyAudio庫從聲卡讀取聲音數據,處理之后再寫回聲卡,這樣就可以在電腦上實時地輸入、處理和輸出聲音數據。想象一下,我們可以做一個小程序,讀取麥克風的數據;加上回聲并和WAV文件中的數據進行混合;最后從聲卡輸出。這不就是一個Karaoke的原型么。
pyAudio是開源聲音庫PortAudio( [http://www.portaudio.com](http://www.portaudio.com) )的Python綁定,目前它只支持阻塞式的輸入輸出模式。所謂阻塞式就是需要用戶的程序主動地去讀寫輸入輸出流。雖然阻塞式在功能上有所局限,但是由于編程比較簡單,非常適合一些處理聲音的腳本程序開發。
### 播放
下面先來看看如何用pyAudio播放聲音。
```
# -*- 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函數有許多參數:
* **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
### 錄音
從聲卡讀取數據和寫入數據一樣簡單,下面我們用一個簡單的聲音監測小程序來展示一下如何用pyAudio讀取聲音數據。
```
# -*- 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的數組。
## 用pyMedia播放Mp3
- 用Python做科學計算
- 軟件包的安裝和介紹
- NumPy-快速處理數據
- SciPy-數值計算庫
- matplotlib-繪制精美的圖表
- Traits-為Python添加類型定義
- TraitsUI-輕松制作用戶界面
- Chaco-交互式圖表
- TVTK-三維可視化數據
- Mayavi-更方便的可視化
- Visual-制作3D演示動畫
- OpenCV-圖像處理和計算機視覺
- Traits使用手冊
- 定義Traits
- Trait事件處理
- 設計自己的Trait編輯器
- Visual使用手冊
- 場景窗口
- 聲音的輸入輸出
- 數字信號系統
- FFT演示程序
- 頻域信號處理
- Ctypes和NumPy
- 自適應濾波器和NLMS模擬
- 單擺和雙擺模擬
- 分形與混沌
- 關于本書的編寫
- 最近更新
- 源程序集
- 三角波的FFT演示
- 在traitsUI中使用的matplotlib控件
- CSV文件數據圖形化工具
- NLMS算法的模擬測試
- 三維標量場觀察器
- 頻譜泄漏和hann窗
- FFT卷積的速度比較
- 二次均衡器設計
- 單擺擺動周期的計算
- 雙擺系統的動畫模擬
- 繪制Mandelbrot集合
- 迭代函數系統的分形
- 繪制L-System的分形圖