先日、Windowsのチューナーアプリを開発してMicrosoft Storeで公開した。
ピッチ検出のアルゴリズムは、ボーカル音程モニターと同じアルゴリズムを使用したが、特定の弦でオクターブエラーが発生することが分かった。
精度改善のため、ギターとベースで様々な音階で録音して、教師データを作成しようと思っていたが、録音とラベルを付ける作業が大変である。
ラベルを付けるのは、ACFとFFTをグラフ化して目視で、誤り区間を見つける必要がある。
自動化したいと思い、最新の精度の高いニューラルネットワークモデルを使用することを考えた。
SwiftF0
最新のモデルを調査したところ、2025年に公開されたSwiftF0が、単一音源のピッチ検出で精度が高いことがわかった。
SwiftF0は、以下のような特徴を備えている。
- 軽量なモデルで、CPUでも高速に処理できる
- 連続的な量としてピッチを出力できる
- オクターブエラーに強い
- ノイズに強い
- MITライセンス
特に、連続的な量として出力できるため、チューナとしても使用できる。
他のニューラルネットワークのピッチ検出モデルでは、ビンを細かく分けることで周波数分解能を上げているが、分類タスクとして学習しているため、値が離散的になり、後処理が必要になる。
SwiftF0は、分類の損失に加えて、連続的な量(対数周波数)を回帰損失としているため、推論時に連続量として出力できる。
また、音素レベルの音声合成を教師データに使うことで、大量のデータと正確なラベルで学習されている。
パラメータ数は95,842と軽量なため、CPUでのリアルタイム解析にも使用できる。
現在のFFTベースのアルゴリズムよりも多少負荷は高くなるが、Windows PC前提であれば多少負荷が高くても実用的なので、アルゴリズムの精度改善をするのではなく、SwiftF0をそのまま組み込むことにする。
SwiftF0を試す
まずは、公開されているSwiftF0を試してみた。
Pythonで、ピッチと信頼度をプロットするプログラムを作成した。
import argparse import numpy as np import soundfile as sf import matplotlib.pyplot as plt from swift_f0 import SwiftF0 def load_wav_mono(path: str): audio, sr = sf.read(path, always_2d=True) audio = audio.mean(axis=1).astype(np.float32) return audio, sr def plot_f0_and_confidence(t, f0, voiced, confidence, title=None, out_path=None, show=True): f0_plot = f0.astype(float).copy() f0_plot[~voiced] = np.nan # 無声区間は非表示 fig, ax1 = plt.subplots(figsize=(12, 4)) # F0(左軸・青) ax1.plot(t, f0_plot, color="tab:blue", label="F0 (Hz)") ax1.set_xlabel("Time (s)") ax1.set_ylabel("F0 (Hz)", color="tab:blue") ax1.tick_params(axis='y', labelcolor="tab:blue") # Confidence(右軸・赤) ax2 = ax1.twinx() ax2.plot(t, confidence, color="tab:red", label="Confidence") ax2.set_ylabel("Confidence", color="tab:red") ax2.set_ylim(0.0, 1.05) ax2.tick_params(axis='y', labelcolor="tab:red") if title: plt.title(title) fig.tight_layout() if out_path: plt.savefig(out_path, dpi=200) if show: plt.show() plt.close(fig) def main(): ap = argparse.ArgumentParser( description="swift-f0でWAVからF0とconfidenceを推定して1枚に色付きプロット" ) ap.add_argument("wav", help="input wav path") ap.add_argument("--fmin", type=float, default=46.875) ap.add_argument("--fmax", type=float, default=2093.75) ap.add_argument("--thr", type=float, default=0.9) ap.add_argument("--out", default=None) ap.add_argument("--no-show", action="store_true") args = ap.parse_args() audio, sr = load_wav_mono(args.wav) detector = SwiftF0( fmin=args.fmin, fmax=args.fmax, confidence_threshold=args.thr ) result = detector.detect_from_array(audio, sr) plot_f0_and_confidence( result.timestamps, result.pitch_hz, result.voicing, result.confidence, title=f"SwiftF0 F0 + Confidence: {args.wav}", out_path=args.out, show=(not args.no_show), ) if __name__ == "__main__": main()
検証1
現在のアルゴリズムで、オクターブエラーが発生している箇所で正しく検出できるか確認した。
現在のアルゴリズムでのエラー箇所

SwiftF0の結果

SwiftF0では、オクターブエラーなしで、一定のピッチが検出できている。
音の立ち上がり部分で、ピッチが変化しているが、小さな変化である。
その前では信頼度が下がっており、有声部分も正しく検出できている。
検証2
連続的なピッチの変化を連続的に出力できるか確認した。
ピッチを揺らして録音した声を使ってピッチ検出した結果は以下の通り。

滑らかな連続的な変化になっている。
まとめ
Windows向けチューナーアプリで発生していた特定弦のオクターブエラーを解消するため、最新のニューラルネットワークモデルであるSwiftF0の導入を検討した。
実験の結果、現在のアルゴリズムでオクターブエラーが発生する箇所でも正確にピッチを検出し、ビブラートのような連続的変化も滑らかに追従できることを確認した。