音高追蹤的目標,是要取得一段音訊當中的音高序列;而抽取出來的音高序列,經常會在後續的一些研究題目,例如哼唱選歌,甚至於音樂生成當中被使用,因此,音高追蹤可以說是音訊處理的基本功之一。我們在基本處理的篇章當中,已經看過一些簡單的、半自動的音高追蹤,而在此篇當中,則將針對追蹤出單一音高(特別是人聲)的狀況,做更仔細一些的相關介紹。如果你有興趣的是同時追蹤多個音高的狀況,例如複數人的合唱或複數樂器的演奏,則請自行搜尋相關研究來參考。本篇接下來的程式範例所使用的音檔,將主要來自 MIR-1K 資料集,所以先請各位自行上網搜尋,並下載此資料集。我們接下來,將會使用該資料集中的一千個歌唱小片段(Wavfile 子資料夾),以及他們的音高標註(PitchLabel 子資料夾)。下載後,記得開啟前述提到的兩個子資料夾,觀察一下音檔以及音高標註的內容,以 PitchLabel/abjones_1_01.pv 為例,其內容應該要有 579 行,每行一個數字且數字大約在 50 上下或者是 0;如果你看到該檔案有 1,158 行,每行有兩個數字,且第二個數字大約在 100~200 上下或 0,代表你可能取得的是不同的版本。
評估音高追蹤的效果好壞時,常見的辦法是準確度。你可能會想問,抽取出的音高是一條序列,標準答案的音高也是一條序列,那麼兩條序列要怎麼定義準確度呢?實際上,對於這兩條音高序列中,某相同位置的單一一個值,我們會說如果這兩個值的差距在一個範圍之內的話,就算是正確,而這個範圍通常會定為四分之一個半音,也就是鋼琴上相鄰兩個鍵(白鍵黑鍵都要算喔!)的差距的四分之一。而整條序列的準確度,除了簡單粗暴的計算答對的比率以外,如果你要處理的音高追蹤是像人唱歌這樣可能偶爾會停下來呼吸,所以不時地會有一小段期間沒有音高的狀況,還可以把準確度分成以下幾種:
- Raw Pitch Accuracy (RPA):標準答案中,有音高的部分的答對比率。沒有音高的部分,不論你的追蹤方法做了什麼判斷,都不去考慮。
- Raw Chroma Accuracy (RCA):與 RPA 類似,只是差距剛好是整數個八度再加減一個小範圍(例如先前提到的四分之一個半音)也算是正確。
- Overall Accuracy (OA):除了 RPA 正確的部分要正確以外,沒有音高的部分,也要能判斷出「這裡沒有音高」才算正確。
舉例來說,假設標準答案是 [60, 60, 60, 61, 61, 63, 63, 0, 0, 65](其中音高的單位為半音,而 0 代表沒有音高),而追蹤出的結果是 [60.1, 59.9, 72.1, 63, 63, 63, 63, 64, 0, 65] 的話,則 RPA 是 8 個裡面對了 5 個,即 62.5%;RCA 是 8 個裡面對了 6 個,即 75%;OA 是 10 個裡面對了 6個,即 60%。以上的計算細節,可以參見下表。你可以看到這三種評估方式的結果各有差距,在實際應用上,你可以視自己的需要,來選擇合適的標準進行評估。
索引值 0 1 2 3 4 5 6 7 8 9 答案 60 60 60 61 61 63 63 0 0 65 預測 60.1 59.9 72.1 63 63 63 63 64 0 65 RPA 正確 正確 錯誤 錯誤 錯誤 正確 正確 不考慮 不考慮 正確 RCA 正確 正確 正確 錯誤 錯誤 正確 正確 不考慮 不考慮 正確 OA 正確 正確 錯誤 錯誤 錯誤 正確 正確 錯誤 正確 正確 如果要用程式計算上述的準確度,則可以使用 mir_eval 函式庫中的 melody.evaluate 函式。它的第一個參數是標準答案的音框時間點,第二個參數是標準答案的音高值(單位是半音,下同),第三個參數是預測出的音高的音框時間點,第四個參數是預測出的音高值。若有需要,你可以加上第五個參數,代表美個音框是有聲音(通常是指人聲)的信心度;若不加此參數,則可以將某個音框的預測音高表示為負值,來代表你的預測方法覺得那個音框可能沒有聲音。以上述範例來說,改使用 mir_eval.melody.evaluate 來計算的方式如下。其中,因為該函式要求的音高輸入單位是 Hz,所以我們需要先把範例中單位為半音的音高做轉換:
import numpy as np from mir_eval.melody import evaluate from mir_eval.util import midi_to_hz ref = np.array([ midi_to_hz(pitch) if pitch != 0 else 0 \ for pitch in [60, 60, 60, 61, 61, 63, 63, 0, 0, 65] ]) pred = np.array([ midi_to_hz(pitch) if pitch != 0 else 0 \ for pitch in [60.1, 59.9, 72.1, 63, 63, 63, 63, 64, 0, 65] ]) scores = evaluate(np.arange(10), ref, np.arange(10), pred) print(scores)