# 音樂情緒辨識系統
## 前言
在本次專案中會向大家展示一個專案,此專案由視窗化程式呈現,可以做到實時的音樂情緒辨識。如同下圖所示:
 ==> 
那要做到畫面所示,需要完成三點需求:
1. 已訓練好的模型
2. 撰寫視窗化程式
3. 專案環境布置
## 模型訓練
在模型訓練一事上,在 [Jetson Xavier NX 介紹](https://hackmd.io/d5Tj7YQTRqC9l2K9ec0MiA)中得知 Jetson Xavier NX 開發版一共只有 384 個 CUDA 核心相較於當前低階顯卡 GeForce RTX 3050 6 GB 的 CUDA 核心有 2304 個而言,由 Jetson Xavier NX 開發版來訓練模型是不太可能的,因此專案的開發流程就會如同下圖:

我們會先在步驟一中用測試機將模型訓練完成,而模型權重則會包裝成檔案存在雲端或實體硬碟中,後續在步驟三 Xavier 上部屬專案時再將模型權重放入其中。
### 專案運行環境
```shell
h5py 3.1.0
joblib 1.2.0
keras 2.10.0
Keras-Preprocessing 1.1.2
matplotlib 3.6.2
numpy 1.21.5
pandas 1.5.1
pip 22.3.1
PyAudio 0.2.12
PyQt-builder 1.14.0
PyQt5 5.15.7
PyQt5-sip 12.11.0
scipy 1.7.3
tensorboard 2.10.1
tensorboard-data-server 0.6.1
tensorboard-plugin-wit 1.8.1
tensorflow 2.10.0
tensorflow-addons 0.19.0.dev0
tensorflow-estimator 2.10.0
tensorflow-io 0.27.0
tensorflow-io-gcs-filesystem 0.27.0
xlrd 2.0.1
```
### 模型任務

模型任務會是將音樂的情緒分類為四種分別是快樂、緊張、悲傷以及平靜。
在了解任務以及專案環境後,便可以進行到[模型程式碼](https://drive.google.com/file/d/1xtGx0EKsh87kay2AEeWzhcLZ1x80iVL4/view?usp=sharing)的部分,訓練出可以分類上述四種情緒的模型,並取得模型權重。
模型訓練的資料來自於: [連結_1](https://github.com/IanChen5273/Music-emotion/tree/main)、[連結_2](https://www.kaggle.com/datasets/cakiki/muse-the-musical-sentiment-dataset)。
## 撰寫視窗化程式
在訓練模型得到權重後,就可以繼續處理視窗化程式的部分。其中如同示意圖一樣,在模型進行預測時同時也要將訊號的音頻畫在視窗中,要做到這一步驟需要用到並行 (Concurrency)。

在 python 中,有 threading 套件可幫助我們完成。
上程式碼:
```python
def on_click(self):
"""
當按下 button 按下時會同時在空格中畫下音頻訊號以及輸出模型預測結果。
其中使用參數 started 來控制當前狀態,預設為 False,當按下 button 後,button 的字串由 Press start 改為 Stopped,參數 started 變為 True 接著由兩個執行緒 (thread) 分別執行畫下音頻訊號以及輸出模型預測結果。
若再次按下 button 代表要停止分類,此時將 started 設為 False 時,兩個執行緒就會停止。之後會把 button 的字串由 Stopped 改為 Press start。
"""
# 參數 started 來控制當前狀態
if not self.started:
self.ui.label.setText('Predicting')
self.started = True
# 讓執行緒 recording_thread 執行畫下音頻訊號
self.recording_thread = threading.Thread(target=self.start_recording,
daemon=True)
# 讓執行緒 predicting_thread 執行分類任務
self.predicting_thread = threading.Thread(target=self.start_predicting,
daemon=True)
# 啟動兩個執行緒
self.recording_thread.start()
self.predicting_thread.start()
else:
self.started = False
self.ui.label.setText('Stopped')
self.ui.pushButton.setEnabled(False)
time.sleep(1)
# wait for both thread stopped
self.recording_thread.join()
self.predicting_thread.join()
self.ui.pushButton.setEnabled(True)
self.ui.label.setText('Press start')
self.ui.pushButton.setText('stop' if self.started else 'start')
def start_recording(self):
"""
在視窗中畫出音頻訊號。
由物件 audio 來執行音頻相關的處理,當呼叫 start 函式時會記錄參數 seconds 秒的音頻訊號,後續只需呼叫 get_np_data 就可以得到近兩秒的音頻訊號名為 np_data,資料型態為 numpy array。
接著將 np_data 透過 matplotlib 套件畫在視窗程式上。
"""
# 設定要記錄的秒數
seconds = 2
# 當 started 為 True,記錄近兩秒的音頻訊號,並畫在視窗程式上
while self.started:
# 呼叫 start 函式時會將近秒的訊號記錄在 audio 的成員裡
self.audio.start(seconds=seconds)
# 透過函式 get_np_data 得到已記錄在成員的訊號
self.np_data = self.audio.get_np_data()
self.data_valid = True
# 透過 matplotlib 套件畫在視窗程式上
self.plt_widget.axis.clear()
times = np.linspace(0, seconds, self.np_data.shape[0])
self.plt_widget.axis.axis('off')
self.plt_widget.axis.set_xlim(0, seconds)
self.plt_widget.axis.plot(times, self.np_data)
self.plt_widget.canvas.draw()
self.data_valid = False
# 畫完後,清除 audio 裡的音頻訊號
self.audio.clear()
print('Recording thread stopped')
# 關閉該執行緒
return
def start_predicting(self):
"""
執行分類任務,並將分類結果輸出在視窗上。
透過參數 data_valid 來控制是否要執行模型,唯有當音頻訊號有兩秒時 (data_valid 是 False 時)才能將訊號輸入至模型計算。
計算完後將結果輸出在視窗上。
"""
while self.started:
# 等待音頻訊號長度是否足夠
while not self.data_valid:
if not self.started:
break
# 將音頻訊號輸入至模型
raw_data = np.array(self.npdata, dtype=float)
# 取得分類結果
p = self.m.predict(raw_data)
# 輸出分類結果
self.ui.label.setText(self.d[p])
print('Predicting thread stopped')
# 關閉該執行緒
return
```
## 專案環境布置
此專案環境已經布置完成並包裝成 Docker Image,只需要打下列指令便可布置好環境:
```shell
# 下載 Docker Image
sudo docker pull q36111265/music_emotion
# 執行容器
sudo docker run --rm -v "專案位置":/home/ubuntu/content -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=$DISPLAY -e GDK_SCALE -e GDK_DPI_SCALE -v /dev/snd:/dev/snd --privileged=true -it q36111265/music_emotion
# 進入專案資料夾
cd content
# 查看專案環境
pip3 list
# 執行專案
python3 XX.py
```
## 結果與討論
在完成專案後可以看到我們完成一個實時音樂情緒辨識系統,其中利用到並行的技巧完成同時顯示音頻訊號以及模型分類的任務。
後續可以精進的方向是提高模型分類的準確度,還有另一點就是當系統持續一段時間後會提示錯誤:
```shell
Exception in thread Thread-1:
Traceback (most recent call last):
File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner
self.run()
File "/usr/lib/python3.8/threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
File "main.py", line 80, in start_recording
self.a.start(seconds=seconds)
File "/home/ubuntu/content/audio_recorder.py", line 28, in start
data = stream.read(self.frames_per_buffer)
File "/home/ubuntu/python3-venv/lib/python3.8/site-packages/pyaudio.py", line 612, in read
return pa.read_stream(self._stream, num_frames, exception_on_overflow)
OSError: [Errno -9981] Input overflowed
```
關於此錯誤從字面上可以理解到是資料溢出, 系統不斷將音頻訊號儲存至記憶體中並將舊的訊號釋放掉,但是儲存的速度比釋放的速度快導致資料溢出。該如何避免此錯誤,歡迎大家提出想法。
目前有相關[文章](https://stackoverflow.com/questions/10733903/pyaudio-input-overflowed)討論,大家可以參考看看。
## 專案改善
根據[文章](https://stackoverflow.com/questions/10733903/pyaudio-input-overflowed)中的提示,顯示 webcam 預設的取樣頻率:
```python
import pyaudio
p = pyaudio.PyAudio()
print(p.get_device_info_by_index(0)['defaultSampleRate'])
Out[1]: 48000.0
```
至此能理解專案發生的問題,因為專案的取樣的頻率是 22050,但是 webcam 的取樣的頻率確是 48000,導致每秒有 48000 筆訊號儲存至記憶體,但是專案只取其中 22050 筆,時間一長就會溢出。
因此改善方式有兩種,一是更改 webcam 的取樣頻率,二是更改 專案的取樣頻率。而第二種需要更改模型的輸入,由此重新訓練模型。