音高追蹤的目標,是要取得一段音訊當中的音高序列;而抽取出來的音高序列,經常會在後續的一些研究題目,例如哼唱選歌,甚至於音樂生成當中被使用,因此,音高追蹤可以說是音訊處理的基本功之一。我們在基本處理的篇章當中,已經看過一些簡單的、半自動的音高追蹤,而在此篇當中,則將針對追蹤出單一音高(特別是人聲)的狀況,做更仔細一些的相關介紹。如果你有興趣的是同時追蹤多個音高的狀況,例如複數人的合唱或複數樂器的演奏,則請自行搜尋相關研究來參考。本篇接下來的程式範例所使用的音檔,將主要來自 MIR-1K 資料集,所以先請各位自行上網搜尋,並下載此資料集。我們接下來,將會使用該資料集中的一千個歌唱小片段(Wavfile 子資料夾),以及他們的音高標註(PitchLabel 子資料夾)。下載後,記得開啟前述提到的兩個子資料夾,觀察一下音檔以及音高標註的內容,以 PitchLabel/abjones_1_01.pv 為例,其內容應該要有 579 行,每行一個數字且數字大約在 50 上下或者是 0;如果你看到該檔案有 1,158 行,每行有兩個數字,且第二個數字大約在 100~200 上下或 0,代表你可能取得的是不同的版本。

評估音高追蹤的效果好壞時,常見的辦法是準確度。你可能會想問,抽取出的音高是一條序列,標準答案的音高也是一條序列,那麼兩條序列要怎麼定義準確度呢?實際上,對於這兩條音高序列中,某相同位置的單一一個值,我們會說如果這兩個值的差距在一個範圍之內的話,就算是正確,而這個範圍通常會定為四分之一個半音,也就是鋼琴上相鄰兩個鍵(白鍵黑鍵都要算喔!)的差距的四分之一。而整條序列的準確度,除了簡單粗暴的計算答對的比率以外,如果你要處理的音高追蹤是像人唱歌這樣可能偶爾會停下來呼吸,所以不時地會有一小段期間沒有音高的狀況,還可以把準確度分成以下幾種:

舉例來說,假設標準答案是 [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%。以上的計算細節,可以參見下表。你可以看到這三種評估方式的結果各有差距,在實際應用上,你可以視自己的需要,來選擇合適的標準進行評估。

索引值0123456789
答案606060616163630065
預測60.159.972.16363636364065
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)