TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

自己相関関数でのピッチ推定の精度向上

以前の日記で、自己相関関数でのピッチ推定の誤差について検証した。

自己相関関数で求めたピッチは、高周波数で誤差が大きくなることを示した。
ここでは、それを改善する方法について検証する。


自己相関関数のピークの位置をnとすると、周波数fは以下の式で計算できる。

{\displaystyle
f = \frac{f_s}{n}
}
f_sは、サンプリング周波数


自己相関関数は、周期的な関数となるため、ピークの整数倍の位置にもピークが現れる。
つまり、ピークのN倍の位置から周波数を求めて、N倍することでも周波数fを求めることができる。

周波数fは、nの逆数の関数であるため、nが大きいほどピッチの誤差が小さくなる。
N倍の位置n_Nのピークを使って周波数fを求める式は以下のようになる。

{\displaystyle
f = \frac{f_s}{n_N} N
}


N倍の位置のピークを使って、ピッチを推定した場合、誤差がどれくらい抑えられるか検証する。

前回の日記で検証した、N倍しないピークを使ったときの誤差と比較するため、周波数の範囲を100Hzから900Hzとして、cent単位で誤差を求める。

import numpy as np
import matplotlib.pyplot as plt
fs = 44100
size = 4096
t = np.arange(0, size) / fs
han = np.hanning(size)

#ピークピッキング
def pick_peak(data):
    peaks_val = []
    peaks_index = []
    for i in range(2, data.size):
        if data[i-1] - data[i-2] >= 0 and data[i] - data[i-1] < 0:
            peaks_val.append(data[i-1])
            peaks_index.append(i-1)
    max_index = peaks_val.index(max(peaks_val))
    return peaks_index[max_index]

import math
errors = []
fset = np.linspace(100, 900, 8001)
for f in fset:
    y = np.sin(2 * np.pi * f * t)
    Y = np.fft.fft(y*han)
    acf = np.fft.ifft(abs(Y)**2)
    n = pick_peak(np.real(acf[0:size/2]))
    n1 = n
    for N in range(2, int(size / 2 / n1) - 1):
        nn = int(n*N)-10 + pick_peak(np.real(acf[int(n*N)-10:int(n*N)+10]))
        n = nn / N
    f0 = fs / n
    cent = (math.log(f0, 2) - math.log(f, 2)) * 12 * 100
    errors.append(cent)
plt.plot(fset, errors)
plt.show()

f:id:TadaoYamaoka:20170114104521p:plain

Nは、ピークのnの整数倍とした場合、誤差が大きくピークの位置からずれるため、2倍、3倍と順番にピークの位置を調べてn/Nの値でnを更新している。
Nの上限は、自己相関関数のフレーム長/2を超えない範囲としている。

比較のために、前回の日記で検証した誤差のグラフを示す。
f:id:TadaoYamaoka:20170107165103p:plain


N倍の位置のピークからピッチ推定した場合、周波数が高くなっても誤差が増えていない。一方、N倍しないピークを使ってピッチ推定した場合は、周波数が高くなるほど誤差が増えている。

N倍の位置のピークからピッチ推定した場合の誤差は、-6.9~7.8centの範囲に抑えられている。

音楽のチューニング用途としては1cent程度までの精度が必要である。
それからするとまだ誤差が大きい。

さらに誤差を小さくするには、Nの値をさらに大きい値にするなどの対応が必要となる。
そのためには、フレーム長を大きくするなどの対応が必要となる。