人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

SPTKの使い方 (3) ピッチ抽出

SPTKの使い方 (2)(2012/7/4)の続き。

今回は、SPTKマニュアルの2章のピッチ抽出(pitch extraction)を試してみます。ピッチとは声の高さのことで、基本周波数F0とも呼ばれます*1。ピッチは、人間の発声におけるアクセント、イントネーション、感情表出などとも密接に関係しているため音声合成において非常に重要なパラメータになっています。

というわけでSPTKのピッチ抽出コマンドpitchを使って、音声からピッチを抽出してみました。

データの変換

使ったデータは、Galatea Talkに付属していたATR 503文の音声データです。本物は有料のようですが、同じテキストで再度録音し直して無償公開しているデータみたい。a01.adからj53.adまで503個のrawファイルが含まれています。

さっそく何て言っているのか確かめるためSPTKの使い方 (1)(2012/7/1)で作ったplay.pyコマンドで再生してみました。が・・・ざーーーーーーー。耳が壊れるかと思った(笑)波形を確かめてみると、

f:id:aidiary:20120707164729p:plain

何かおかしい。サンプリング周波数(16000Hz)も量子化ビット数(16bit)もチャンネル数(モノラル)もあっていて、WaveSurferでは正常に再生できるのになぜ?と悩んでいたが、どうやらエンディアンの違いだと気づいた・・・このrawファイルはビッグエンディアンだった。というわけでSPTKのswabコマンドですべてリトルエンディアンに変換した。

#coding:utf-8
import os

if not os.path.exists("speech2"):
    os.mkdir("speech2")

for file in os.listdir("speech"):
    source = os.path.join("speech", file)
    target = os.path.join("speech2", file)
    os.system("swab +s < %s > %s" % (source, target))

これで音声が聞ける。

python play.py a01.ad
「あらゆる現実をすべて自分の方へねじ曲げたのだ。」

えっ、何それ(笑)他のも聞いてみたけど脈絡のない変な文章が多い。ちょっと調べてみると音声界隈では有名な文章らしい(あらゆる現実の話)。

ピッチ抽出

というわけでこの音声からピッチを抽出してみよう。

x2x +sf a01.ad | pitch -a 1 -p 80 -s 16 -L 60 -H 240 > a01.pitch
fdrw -y 0 250 -W 1.5 -H 0.4 < a01.pitch | xgr

pitchの各オプションの意味は、

-a 1     swipeというピッチ抽出アルゴリズムを使用
-p 80    80サンプルおきにフレームシフト
-s 16    サンプリング周波数(16kHz)
-L 60    探索する最小周波数(60Hz)
-H 240   探索する最大周波数(240Hz)

ピッチ抽出のアルゴリズムは、snackとswipeというのが実装されていました。swipeの元論文は、

音楽でも使えるのかな?ちょっと興味ある。今回は男声だから探索の範囲が60Hzから240Hzでよかったけれど、女声の場合だともっと高くしないとダメかな。60Hzから240Hzってけっこう低い声だ。実行すると下のようなピッチ軌跡のグラフが表示されました。

f:id:aidiary:20120707171051p:plain

XWindowsの描画はいまいち華がないので、matplotlibでも描画してみました。音声波形をプロットするときは、2バイト(short)ずつ読み込んでましたが、ピッチファイルは浮動小数点のバイナリがずらずら入っているので4バイト(float)ずつ読み込んでます。

#coding:utf-8
import struct
import sys
from pylab import *

if len(sys.argv) != 2:
    print "usage: python draw_pitch.py [pitch file]"
    sys.exit()
pitch_file = sys.argv[1]

# ピッチファイルをロード
pitch = []
f = open(pitch_file, "rb")
while True:
    # 4バイト(FLOAT)ずつ読み込む
    b = f.read(4)
    if b == "": break;
    # 読み込んだデータをFLOAT型(f)でアンパック
    val = struct.unpack("f", b)[0]
    pitch.append(val)
f.close()

# プロット
plot(range(len(pitch)), pitch)
xlabel("time (frame)")
ylabel("F0 (Hz)")
show()

f:id:aidiary:20120707174938p:plain

横軸の単位はフレームになります。pitchコマンドでは、重複なしで80サンプル(1フレーム)ごとフレームシフトしながらピッチ抽出しているようです。元の音声が60670サンプルあったので、最終的に得られるピッチの数は60670/80=758となります。ピッチファイルをdmpするとちゃんと758フレームあることが確認できました。あと、ピッチが0のところは無声音の区間ですね。ピッチは有声音からしか得られないので。けっこうきれいにとれているみたいです。

そういえば、ATR 503文をランダムにツイートするボットを発見しました。@ATR503_bot。最初は面白くてフォローしてたんだけどだんだんうざくなってきたので外しちゃった(笑)

補足

縦軸をf0(Hz)と書いてしまったのですが、これは誤りです。pitchコマンドに何もオプションを指定しないとサンプリング周波数をHzで割った値(pitch)が出力されます。Hzで出力したいときは-o 1というオプションが必要です。ピッチとF0って同じ意味だと思ってたけど違うのかな?

SPTKのpitchのソースコードを見るとわかります。

SPTK-3.6/bin/pitch/snack/jkGetF0.c L2117

  for (i = 0; i < fnum; i++) {
      switch (otype) {
      case 1:                   /* f0 */
          fwrite(tmp + i, sizeof(float), 1, stdout);
          break;
      case 2:                   /* log(f0) */
          if (tmp[i] != 0.0) {
              tmp[i] = log(tmp[i]);
          } else {
              tmp[i] = -1.0E10;
          }
          fwrite(tmp + i, sizeof(float), 1, stdout);
          break;
      default:                  /* pitch */
          if (tmp[i] != 0.0) {
              tmp[i] = sample_freq / tmp[i];
          }
          fwrite(tmp + i, sizeof(float), 1, stdout);
          break;
      }
  }

*1:厳密には、基本周波数は物理量、ピッチは心理量なので異なる概念とのことです(参考:ピッチと基本周波数はどう違うのですか?)。ピッチ抽出といってもやっていることは物理量の基本周波数(F0)の抽出なので使い分けはあまり厳密ではないのかな?