人工知能に関する断創録

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

SPTKの使い方 (7) メルケプストラム分析合成

SPTKの使い方 (6)(2012/8/5)の続き。

今回は、SPTK(2012/7/1)を使って音声合成の実験をしてみました。ここで言う音声合成は、テキストを音声に変換するTTS(Text-to-Speech)ではなく、分析合成と呼ばれるものです。

分析合成では、人間の音声からパラメータを抽出し、ソース・フィルタモデル(Wikipedia)を用いて音声を再合成します。いわゆるヴォコーダーという技術です。ヴォコーダー(Wikipedia)を調べると

本来の意味は通信用の音声圧縮技術で、携帯電話などの多くの機器で使用されている。音声の波形を直接送るのではなくパラメータ化して送り、受信側ではそれらのパラメータから元の音声を合成する。

ヴォコーダー(Wikipedia)

とあります。ん?ということは、携帯電話で聞いているのは実際の肉声ではなく、パラメータから再合成した合成音声なのですかね?これは興味深い。

今まで取り上げてきたSPTKというオープンソースのライブラリを使うと簡単に分析合成の実験をすることができます。SPTKには、

  • メルケプストラム
  • LPC
  • PARCOR
  • LSP
  • メル一般化ケプストラム
  • メル周波数ケプストラム(MFCC)

という6つのパラメータが実装されています。今回は、一番上のメルケプストラムというパラメータで分析合成を試します。残りは次回以降取り上げます。

ソース・フィルタモデル

人間の音声は、ノドの声帯を振動させてブザーのような音を出し、その音が声道、口、唇を通過することで「こんにちは」などの音声が出てくる仕組みになっています。これを数学的にモデル化したのがソース・フィルタモデルです。


f:id:aidiary:20130119110259p:plain
http://www.kumikomi.net/archives/2010/08/ep30gose.php から引用

音源で声帯振動にあたるブザー音を作り出し、ブザー音をディジタルフィルタに通すことで合成音を作ります。音源では、声の高さを決めるピッチというパラメータを使い、ディジタルフィルタでは声道特性を表すメルケプストラムなどのパラメータを使います。


f:id:aidiary:20130119105645p:plain
http://www.kumikomi.net/archives/2010/08/ep30gose.php から引用

音声ファイルの準備

(2012/7/1)にも使いましたが、SPTKに付属しているサンプル音声を例に試してみます。data.shortというRAWファイルです。こんな音声です。

音声フォーマットは、16000Hz、16bit、モノラル、リトルエンディアンです。もし、WAVEファイルしかないなら、WAVEファイルをSPTKで使えるRAWファイルに変換します。SPTKのwave2rawを使うと簡単です。

# WAVEファイルをRAWファイルに変換する
# 出力はdata.raw
wav2raw data.wav

逆にRAWファイルをWAVEファイルに戻すにはsoxを使います。RAWファイルには、ファイルフォーマットの情報がないので引数で指定します。

# RAWファイルをWAVEファイルに変換
sox -e signed-integer -c 1 -b 16 -r 16000 data.raw data.wav

ピッチ抽出

まず、音源に関するパラメータであるピッチをpitchコマンドで抽出します。ピッチは声の高さを表すパラメータです。これはピッチ抽出(2012/7/7)ですでに取り上げました。

x2x +sf data.raw | pitch -a 1 > data.pitch
dmp +f data.pitch > pitch.txt

data.pitchはバイナリファイルなのでテキストファイルにダンプしています。gnuplotやmatplotlibで描画できます(後述のスクリプト参照)。


f:id:aidiary:20130119105950p:plain

縦軸のpitchは、はじめHzだと思い込んでいたのですが違いました。サンプリング周波数をHzで割った値のようです。なので高い音ほど値が小さくなります。

音源の生成

exciteコマンドでピッチから音源のブザー音を作成します。soprというコマンドで振幅を1000倍し、x2xコマンドでfloatをsigned integerに変換することでRAWファイルを作成しています。

excite -p 80 data.pitch | sopr -m 1000 | x2x +fs > source.raw
sox -e signed-integer -c 1 -b 16 -r 16000 source.raw source.wav

こんな音です。

あー、何となく青い植木鉢って言いたそうなのは伝わります(笑)ついでにソースの波形も描画してみます。


f:id:aidiary:20130119121913p:plain

ピッチがある区間(有声音)はパルスが立っていて、ピッチがない区間(無声音)はつぶれて見えませんがホワイトノイズになっていることがわかります。パルスはブザーみたいな音、ホワイトノイズはシャーって音に聞こえます。

pitchの値が小さい部分(高い声)はパルス間隔が短く、pitchの値が大きい部分(低い声)はパルス間隔が広いようです。うーん、音源の振幅がピッチの形と似ているのはなぜなんだろう?音の高さを表すピッチと音の大きさを表す振幅は関係無いような気がするんだけど・・・まだまだ勉強不足かも。

この音源を声道パラメータを指定したフィルタに通すと合成音になります。ほんとかな?

メルケプストラムの抽出

メルケプストラムは、声道特性を表すパラメータです。mcepコマンドで抽出できます。音声をframeコマンドでフレームに分割し、windowsコマンドで窓をかけてから、mcepで抽出します。

x2x +sf < data.raw | frame -l 400 -p 80 | window -l 400 -L 512 | mcep -l 512 -m 20 -a 0.42 > data.mcep
  # -m : 20次元のメルケプストラム
  # -a : 16000Hzの音声は0.42で固定

data.mcepがメルケプストラムパラメータです。メルケプストラムは、時間ごとに変化していくパラメータです。あとで時間変化に従って、スペクトルとメルケプストラムがどのように変化するか動画で確認できるスクリプトを作る予定ですが、ここでは、音声の65フレーム目の瞬間のスペクトルとメルスペクトラムだけを切り取って図示してみます。

x2x +sf < data.raw | frame -l 400 -p 80 | bcut +f -l 400 -s 65 -e 65 | window -l 400 -L 512 | spec -l 512 | dmp +f > spec.txt
bcut +f -n 20 -s 65 -e 65 < data.mcep | mgc2sp -m 20 -a 0.42 -g 0 -l 512 | dmp +f > mcep.txt

specは音声のスペクトルを求めるコマンド、mgc2spはメルケプストラムパラメータをスペクトルに変換するコマンドです。dmpでテキストファイルにダンプしてからgnuplotやmatplotlibでグラフ描画できます。たとえば、gnuplotなら

gnuplot> plot "spec.txt" w l
gnuplot> replot "mcep.txt" w l

でグラフが描けます。


f:id:aidiary:20130119111918p:plain

青がスペクトル、赤がメルケプストラムです。メルケプストラムは、スペクトルの形状(スペクトル包絡)を表すパラメータだということがわかります。スペクトル形状の違いによって音韻(あいうえおなど)が区別されます。

メルケプストラム分析合成

最後に音源とメルケプストラムパラメータから音声を合成します。mlsadfがフィルタのコマンドです。MLSAフィルタというものらしいんですが詳細は勉強中です。data.mcep.rawが分析合成音です。

excite -p 80 data.pitch | mlsadf -m 20 -a 0.42 -p 80 data.mcep | x2x +fs > data.mcep.raw
sox -e signed-integer -c 1 -b 16 -r 16000 data.mcep.raw data.mcep.wav

ではオリジナルの音声と分析合成音を比べてみます。

オリジナルの音声

分析合成音

かなりよく再合成できているのではないでしょうか?

HMM音声合成というのは、ピッチやメルケプストラムのパラメータを確率モデル(HMM)を使って生成する音声合成法のようです。確率モデルの学習や発声内容の時系列に沿ったパラメータ生成が必要なため、分析合成より難易度が高そうです。

Pythonスクリプト

今までのをすべてまとめたPythonスクリプトです。

python mcep.py data.raw 20

のようにRAWファイル(16000Hz、16bit、モノラル、リトルエンディアン)とメルケプストラムの次数(20)を入れて使います。

#coding:utf-8
import os
import sys
import subprocess
import pylab

# メルケプストラム分析合成の手順
# 実行にはsox、SPTK、matplotlibが必要
# 使い方: python mcep.py [raw_file] [order of mel cepstrum]

def execute(cmd):
    """コマンドを実行"""
    subprocess.call(cmd, shell=True)
    print cmd

def draw_figure(dat_file, xlabel="", ylabel="", style="b-", lw=1):
    """グラフ描画"""
    fp = open(dat_file, "r")
    x = []
    y = []
    for line in fp:
        line = line.rstrip()
        dat = line.split()
        x.append(float(dat[0]))
        y.append(float(dat[1]))
    pylab.plot(x, y, style, linewidth=lw)
    pylab.xlabel(xlabel)
    pylab.ylabel(ylabel)
    pylab.xlim(min(x), max(x))
    fp.close()

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print "usage: python mcep.py [raw_file] [order of mel cepstrum]"
        exit()

    raw_file = sys.argv[1]    # RAWファイル
    order = int(sys.argv[2])  # メルケプストラムの次数

    # 16000Hzの場合は0.42で固定
    alpha = 0.42

    prefix = raw_file.replace(".raw", "")

    # RAWファイルをWAVEファイルに変換
    # 音声を確認できるように
    cmd = "sox -e signed-integer -c 1 -b 16 -r 16000 %s.raw %s.wav" % (prefix, prefix)
    execute(cmd)

    # ピッチ抽出
    cmd = "x2x +sf %s.raw | pitch -a 1 > %s.pitch" % (prefix, prefix)
    execute(cmd)

    # ピッチの描画
    cmd = "dmp +f %s.pitch > pitch.txt" % prefix
    execute(cmd)
    draw_figure("pitch.txt", "frame", "pitch")
    pylab.savefig("pitch.png")
    pylab.clf()
    os.remove("pitch.txt")

    # 音源の生成
    cmd = "excite -p 80 data.pitch | sopr -m 1000 | x2x +fs > source.raw"
    execute(cmd)
    cmd = "sox -e signed-integer -c 1 -b 16 -r 16000 source.raw source.wav"
    execute(cmd)
    cmd = "dmp +s source.raw > source.txt"
    execute(cmd)

    # 音源の描画
    draw_figure("source.txt", "sample", "amplitude")
    pylab.savefig("source.png")
    pylab.clf()
    os.remove("source.txt")

    # メルケプストラム分析
    cmd = "x2x +sf < %s.raw | frame -l 400 -p 80 | window -l 400 -L 512 | mcep -l 512 -m %d -a %f > %s.mcep" % (prefix, order, alpha, prefix)
    execute(cmd)

    # 65フレームのの対数スペクトルを描画
    cmd = "x2x +sf < %s.raw | frame -l 400 -p 80 | bcut +f -l 400 -s 65 -e 65 | window -l 400 -L 512 | spec -l 512 | dmp +f > spec.txt" % prefix
    execute(cmd)
    draw_figure("spec.txt", style="b-")

    # フレーム65のメルケプストラムのスペクトル包絡を描画
    cmd = "bcut +f -n %d -s 65 -e 65 < %s.mcep | mgc2sp -m %d -a %f -g 0 -l 512 | dmp +f > mcep.txt" % (order, prefix, order, alpha)
    execute(cmd)
    draw_figure("mcep.txt", "frequency bin", "log magnitude [dB]", style="r-", lw=2)
    pylab.savefig("mcep.png")

    os.remove("spec.txt")
    os.remove("mcep.txt")

    # 分析合成音の作成
    cmd = "excite -p 80 %s.pitch | mlsadf -m %d -a %f -p 80 %s.mcep | x2x +fs > %s.mcep.raw" % (prefix, order, alpha, prefix, prefix)
    execute(cmd)

    # WAVEファイルに変換
    cmd = "sox -e signed-integer -c 1 -b 16 -r 16000 %s.mcep.raw %s.mcep.wav" % (prefix, prefix)
    execute(cmd)

次回はメルケプストラムの次数を変えると音声がどう変わるか実験してみようと思います。

関連リンク