<style> .markdown-body table{ display: unset; } </style> # 使用 Python 產生聲音及繪製頻譜圖 > 作者:王一哲 > 日期:2019/8/13 ## 前言 我在去年看到了啾啾鞋講解電話按鍵聲的影片[[1](https://youtu.be/48xx6H-M4ZI)],裡面用到的技術稱為**雙音多頻訊號** (Dual-Tone Multi-Frequency, **DTMF**)[[2]( https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling)],各個按鍵對應的聲音頻率如下表所示 <div style="text-align:center"> | | 1209 Hz | 1336 Hz | 1477 Hz | 1633 Hz | |----------|---------|---------|---------|---------| |**697 Hz**| 1 | 2 | 3 | A | |**770 Hz**| 4 | 5 | 6 | B | |**852 Hz**| 7 | 8 | 9 | C | |**941 Hz**| * | 0 | # | D | </div> <br /> 另外也在 YouTube 上找到了成功大學數學系舒宇宸教授演講的影片[[3](https://youtu.be/475jWptKBtM?t=3192)],教授在演講時用 MATLAB 產生了上述表格中的聲音。既然用 MATLAB 可以做到,應該也可以用 Python 做到同樣的效果才對,於是我花了一天的時間研究了一下,找到以下的解決方案。 <br /> ## 安裝所需套件 ### PyAudio 產生聲音會用到的套件是 **PyAudio** [[4]( http://people.csail.mit.edu/hubert/pyaudio/)],在 Windows 10 可以使用 **Windows PowerShell(系統管理員)** 輸入以下指令安裝 ```shell pip install pyaudio ``` 在 Debian/Ubuntu 則是輸入以下指令安裝 ```shell sudo apt-get install python-pyaudio python3-pyaudio ``` 如果沒有辦法抓到最新版的 PyAudio,可以改用以下指令安裝 ```shell pip3 install pyaudio ``` 我在 Linux Mint 19.1 Cinnamon 測試過用 apt-get 安裝 pyaudio 的指令,可以成功安裝套件。 <br /> ### PySoundFile 為了將產生的聲音儲存成音效檔,我選用的是 **PySoundFile** [[5](https://pysoundfile.readthedocs.io/en/0.9.0/)],在 Windows 10 可以使用 **Windows PowerShell(系統管理員)** 輸入以下指令安裝 ```shell pip install soundfile ``` 在 Linux 則是輸入以下指令安裝 ```shell pip3 install pysoundfile ``` 我在 Linux Mint 19.1 Cinnamon 測試以上的指令,可以成功安裝套件。 <br /> ## 產生聲音並儲存為音效檔 以下的程式碼主要是參考**子風的知識庫 [Python] Pyaudio 教學** [[6]](https://zwindr.blogspot.com/2017/03/python-pyaudio.html) 以及 **Stack Overflow** 網站上的文章 [[7](https://stackoverflow.com/questions/45025892/sound-generated-not-being-saved-to-a-file-as-it-should)] 改寫而成。 <br /> ```python= import numpy as np import pyaudio import soundfile as sf # DTMF 頻率對照表 freq = {'1': [1209, 697], '2': [1336, 697], '3': [1477, 697], 'A': [1633, 697], '4': [1209, 770], '5': [1336, 770], '6': [1477, 770], 'B': [1633, 770], '7': [1209, 852], '8': [1336, 852], '9': [1477, 852], 'C': [1633, 852], '*': [1209, 941], '0': [1336, 941], '#': [1477, 941], 'D': [1633, 941]} # 定義正弦波 def sine(frequency, duration, rate): n = int(duration * rate) interval = 2 * np.pi * frequency / rate return np.arange(n) / rate, np.sin(np.arange(n) * interval) # 產生兩個特定頻率的聲音資料 def generator(f1=697, f2=1209, duration=1, rate=44100, start=0): t1, d1 = sine(f1, duration, rate) t2, d2 = sine(f2, duration, rate) data = np.stack((t1+start, d1, d2), axis=1) return data # 產生寫入 stream 的資料 names = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#', 'A', 'B', 'C', 'D'] data = [] duration = 0.2 for i in range(len(names)): tmp = generator(f1=freq[names[i]][0], f2=freq[names[i]][1], duration=duration, start=i*duration) data = np.append(data, tmp) data = data.reshape(int(len(data)/3), 3) # 產生聲音 p = pyaudio.PyAudio() stream = p.open(format=pyaudio.paFloat32, channels=2, rate=44100, output=True) stream.write(data[:, 1:].astype(np.float32).tostring()) stream.stop_stream() stream.close() p.terminate() # 儲存資料檔 np.savetxt('DTMF_data.csv', data, delimiter=',') # 儲存 wav 檔 sf.write('DTMF_sound.wav', data[:, 1:], 44100, subtype='PCM_24') ``` <br /> 1. 第 6 ~ 9 行:用字典格式儲存每個按鍵對應的聲音頻率。 2. 第 12 ~ 15 行:定義正弦波,輸入項目為**聲音頻率** frequency (Hz)、**播放時間長度** duration (s)、**取樣頻率** rate (Hz),回傳項目為**時刻**及**振幅**。函式中 n 為**播放數量**, interval 為**取樣間隔**,由於 sin 裡面要放的是 $\theta = \omega t$ ,因此取樣間隔定義為 $(2 \pi f) / rate = \omega / rate$。 3. 第 18 ~ 22 行:產生兩個特定頻率的聲音資料,輸入項目為**兩個指定的頻率** f1 及 f2、**播放時間長度** duration、**取樣頻率** rate、**開始播放時刻** start,回傳項目為**時刻以及兩個頻率聲波對應的振幅**。 4. 第 25 ~ 34 行:產生寫入 stream 的資料。 5. 第 37 ~ 42 行:產生聲音,執行程式時會聽到 0 ~ 9 以及 \*、\#、A、B、C、D 對應的聲音。 6. 第 45 行:儲存資料檔,之後要畫頻譜圖。 7. 第 48 行:儲存 wav 檔。 <br /> ## 繪製頻譜圖 假設剛才將資料儲存成文字檔 DTMF_data.csv,我們可以用以下的程式碼繪製頻譜圖。 ```python= import matplotlib.pyplot as plt import matplotlib as mpl import numpy as np # 讀取資料 t, d1, d2 = np.loadtxt('DTMF_data.csv', dtype=float, delimiter=',', unpack=True) # 產生繪圖物件 fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15,8), dpi=72, sharex=True) # 時刻 - 振幅關係圖 amp = d1 + d2 ax1.plot(t, amp, linestyle='-', color='blue') ax1.set_xlabel('time (s)', fontsize=14) ax1.set_ylabel('amplitude', fontsize=14) # 頻譜圖 cmap = mpl.cm.cool Fs = int(1.0 / (t[1]-t[0])) data, freqs, bins, im = ax2.specgram(amp, cmap=cmap, NFFT=1024, Fs=Fs, noverlap=128) ax2.set_ylabel('frequency (Hz)', fontsize=14) ax2.axis(ymax=2000) fig.colorbar(im, ax=ax2, shrink=0.6, orientation='horizontal', pad=0.2) # 儲存及顯示圖片 fig.savefig('DTMF_specgram.png') fig.show() ``` <br /> 1. 第 6 行:從 DTMF_data.csv 讀取資料,將資料按照欄位依序存入陣列 t、d1、d2。 2. 第 9 行:產生繪圖物件,兩張小圖使用同樣的 x 軸。 3. 第 12 ~ 15 行:繪製時刻 - 振幅關係圖。 4. 第 18 ~ 23 行:使用 **numpy.specgram** 繪製頻譜圖,以下是簡單的語法,詳細的語法請參考官方說明書 [[8](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.specgram.html)] 及範例 [[9](https://matplotlib.org/3.1.1/gallery/images_contours_and_fields/specgram_demo.html#sphx-glr-gallery-images-contours-and-fields-specgram-demo-py)]。 ```python numpy.specgram([資料], cmap=[Colormaps 設定], NFFT=[計算 FFT 時每個區塊中的資料點數量], Fs=[取樣頻率], noverlap=[兩個區塊重疊的資料點數量]) ``` 5. 第 26 ~ 27 行:儲存及顯示圖片。 <br /> 以下是輸出的圖片。由於資料點太密集,時刻 - 振幅關係圖的線條全部擠在一起,看不出什麼特徵。從頻譜圖可以看出資料分為16個區塊,每個區塊中看起來有兩個頻率的強度較強,如果將 y 軸最大值限制為 2000,可以更清楚地看到這些頻率,我們再跟文章最前面的 DTMF 表格對照看看,應該可以看出每一個區塊對應的按鍵。 <br /> <img height="100%" width="100%" src="https://imgur.com/rIne5Oi.png" style="display: block; margin-left: auto; margin-right: auto;"/> <div style="text-align:center">上方為時刻 - 振幅關係圖,下方為頻譜圖(不限制 y 軸範圍)</div><br /> <img height="100%" width="100%" src="https://imgur.com/RbcBQM5.png" style="display: block; margin-left: auto; margin-right: auto;"/> <div style="text-align:center">上方為時刻 - 振幅關係圖,下方為頻譜圖(y 軸最大值為 2000)</div><br /> ## 結語 PyAudio 的功能很多,除了產生聲音之外,可以用來讀取、處理音效檔,也可配合麥克風錄音。之後應該可以用 PyAudio 和 Raspberry Pi 做一些有趣的專案。 <br /> ## 參考資料 1. [電話按鍵聲的秘密,原來不只是響好聽的? | 超邊緣冷知識 第35集 | 啾啾鞋](https://youtu.be/48xx6H-M4ZI) 2. [WikiPedia DTMF]( https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling) 3. [探索10-8講座:醫學影像中的數學 / 舒宇宸助理教授](https://youtu.be/475jWptKBtM?t=3192) 4. [PyAudio 官方說明書]( http://people.csail.mit.edu/hubert/pyaudio/) 5. [PySoundFile 官方說明書](https://pysoundfile.readthedocs.io/en/0.9.0/) 6. [子風的知識庫 [Python] Pyaudio 教學](https://zwindr.blogspot.com/2017/03/python-pyaudio.html) 7. [Stack Overflow](https://stackoverflow.com/questions/45025892/sound-generated-not-being-saved-to-a-file-as-it-should) 8. [matplotlib.pyplot.specgram 官方說明書](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.specgram.html) 9. [matplotlib.pyplot.specgram 範例](https://matplotlib.org/3.1.1/gallery/images_contours_and_fields/specgram_demo.html#sphx-glr-gallery-images-contours-and-fields-specgram-demo-py) 10. [Matplotlib Colormaps 官方教學文件](https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html) --- ###### tags:`Python`