頻率域(frequency domain)的音高追蹤,是指先把音訊轉成頻譜,然後在頻譜上進行音高追蹤。頻率域的音高追蹤也有幾種不同的做法,本篇將只說明 HPS (Harmonic Product Spectrum)這種方法。HPS 的概念是利用泛音會出現在基頻(找出基頻就是音高追蹤的目標)的整數倍的頻率上的的原理,把原本的頻譜跟自己的二分之一、三分之一、四分之一、…的壓縮版本疊加起來,就可以把每個泛音都疊到基頻上,讓基頻的位置特別突出,就可以用疊加結果的最大值的出現位置來換算出頻率。

我們使用小提琴演奏出的 261 Hz (C4)訊號,並框出某一個特定的 frame 來轉成頻譜,以及畫出它的二分之一、三分之一、四分之一、…的壓縮版本,以及疊加的結果:

import matplotlib.pyplot as plt
import numpy as np
import scipy.io.wavfile

WAV_PATH = 'violin-C4.wav'
FRAME_SIZE = 512
sample_start = 20000
sample_end = sample_start + FRAME_SIZE

fs, y = scipy.io.wavfile.read(WAV_PATH)
print(fs, y.dtype, y.shape)

y = y[sample_start:sample_end] / 32768
spectrum = np.fft.rfft(y)
spectrum = np.abs(spectrum)

sum_result = np.zeros_like(spectrum)
for i in range(1, 6):
	resampled_spectrum = spectrum[::i]
	plt.subplot(6, 1, i)
	plt.plot(resampled_spectrum, '.-')
	plt.xlim(0, spectrum.size)
	sum_result[:resampled_spectrum.size] += resampled_spectrum

plt.subplot(6, 1, 6)
plt.plot(sum_result, '.-')
plt.xlim(0, spectrum.size)

print('Max idx position:', np.argmax(sum_result))

plt.show()

在上述範例中:

你接著可能會問,上面範例跑出來的最大位置是索引值 12 的地方,那然後呢?這就要牽涉到傅立葉轉換的數學原理了。假設你輸入的 frame 大小是 512 的話,則傅立葉轉換會給你「除以二再加一」個點,也就是 257 個點(實際上會是 512 個點,但以音訊這類輸入是實數的狀況來說,另一半是對稱的,所以不必考慮),而這 257 個點會平均代表 0 到取樣率的一半的頻率,所以搭配上範例音檔的取樣率為 11,025 Hz 時,索引值 12 代表的是 (11025 / 2) / (257 - 1) * 12 = 258.4 Hz,此結果跟原始的 261 Hz 相當接近,只是由於傅立葉轉換的數學性質限制,而難免有些誤差存在。若要改善此問題,你可以將抓出來的音框補零到 1024 或更後面的幾個二的次方數,或者直接使用長一點的音框來分析。你也可以試著將頻率域的音高追蹤用於人聲,但因為可能需要更多技巧來避免誤差,因此本篇暫不提及。