音声変換の進展（備忘録も兼ねて）

備忘録も兼ねてるので結構雑なのは許してクレメンス。

# これまでのお話
[こんなこと](https://qiita.com/lefirea/items/4f0e3912862b8958e896)をやってたり、やっぱりダメだったので[CycleGan-VC2](http://www.kecl.ntt.co.jp/people/kaneko.takuhiro/projects/cyclegan-vc2/index.html)を参考にしたモデルで試したりしてた。
使ってた特徴量はメルケプストラム。この辺もCycleGAN-VC2を参考にして選択。

が、ダメ。

モデル調整を頑張ってみたり、メルケプストラムをNMFしてみたり。STFTしたもの（メルケプまで行かない）を入力してみたり、これもNMFしてみたり。なんか色々やったけど、終ぞ上手くいかず、そして萎えて断念。

# 今回のお話
断念してから1年。
ようやく新天地にたどり着いた。
前々から「LPCという特徴量がフィルタ的な役割でもある」とは聞いてた。音響管とかそういう話。
で、それを思い出し、調べ直した結果。[このスライド](https://www.slideshare.net/akinoriito549/ss-23821843)の13枚め、「フォルマント合成 ~Formant Synthesis~」を見てﾋﾞﾋﾞｯと来た。

<iframe src="//www.slideshare.net/slideshow/embed_code/key/xb0oXddwJOR3Uc?startSlide=14" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="yes" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe>

フィルタってそういうことか

と。

こっからは早かった。
いそいそと[pysptk](https://github.com/r9y9/pysptk)をインストールし、[チュートリアル](https://nbviewer.org/github/r9y9/pysptk/blob/51c103e5a7e9746c96cd78043df4e48fe2d6a3a8/examples/Speech%20analysis%20and%20re-synthesis.ipynb)と[ドキュメント](https://pysptk.readthedocs.io/en/latest/)を見ながらコーディング。

前に用意しておいたペアデータ音声からそれぞれLPCを求め、音声1にLPC2を、音声2にLPC1を与えて音声を再合成。
するとどうだろう。なんと声質が入れ替わって聞こえるではないか！
しかしまだ安心はできない。CycleGANを使うことを考えると、ノンペアデータでもそうなる必要がある。
ということで、発話内容が違い、話者も違う音声２つを用意して同様の実験を行ってみた。
結果、声質はおろか、発話内容も維持されているように聞こえるじゃないか。

こりゃもう勝ったな。風呂入ってくる。

ということで、大きな進展がありました。という話。

# プログラム
## 音声読み込み
難しいことは無い。scipyのwavfileを使用する。

```python
from scipy.io import wavfile

x, fs = wavfile.read("filename.wav")
```

ついでに、長さを揃える場合はこんな感じ。

```python
x1 = x1[fs1//2:fs1//2+fs1]
```

## 音声のフレーム化
要は窓関数を掛けたりする。

```python
import librosa

def toFrames(x, frameLength=1024, hopLength=80, windowFunction=pysptk.blackman):
    frames = librosa.util.frame(x, frame_length=frameLength, hop_length=hopLength).astype(np.float64).T
    frames *= windowFunction(frameLength)
    return frames
```

今回はpysptkのチュートリアルに従ってブラックマンを採用。なんでブラックマンなのか、窓関数ごとの違いについては後で勉強しようと思う。

## LPCを求める
基本的にはチュートリアルのままだが、`np.apply_along_axis`関数の調子が悪かった（というかpysptkの中でエラーを吐く）ので愚直にforループ使って実装。

```python
import numpy as np
import pysptk

def toLPC(frames, order=25):
    for i, frame in enumerate(frames):
        frame += np.random.randn(len(frame)) * 10e-6
        if i == 0:
            lpc = pysptk.lpc(frame, order=order)
        else:
            lpc = np.vstack([lpc, pysptk.lpc(frame, order=order)])

    lpc[:, 0] = np.log(lpc[:, 0])
    return lpc
```

あとついでにf0も求めておく。

```python
hopLength = 80
f0 = pysptk.swipe(x.astype(np.float64), fs=fs, hopsize=hopLength, min=50, max=500)
```

## LPCを音声に再合成
ここも基本的にはチュートリアルのまま。

```python
from pysas import excite

def toWave(x, fs, lpc, f0, output="synthed.wav", order=25, hopLength=80):
    generator = excite.ExcitePulse(fs, hopLength, False)
    sourceExcitation = generator.gen(f0)

    synthesizer = pysptk.synthesis.Synthesizer(pysptk.synthesis.AllPoleDF(order=order), hopLength)
    synthed = synthesizer.synthesis(sourceExcitation, lpc)
    wavfile.write(output, fs, synthed.astype(np.int16))
```

## 統合したもの

```python
import numpy as np
import pysptk
from scipy.io import wavfile
import librosa
from pysas import excite


def toFrames(x, frameLength=1024, hopLength=80, windowFunction=pysptk.blackman):
    frames = librosa.util.frame(x, frame_length=frameLength, hop_length=hopLength).astype(np.float64).T
    frames *= windowFunction(frameLength)
    return frames


def toLPC(frames, order=25):
    for i, frame in enumerate(frames):
        frame += np.random.randn(len(frame)) * 10e-6
        if i == 0:
            lpc = pysptk.lpc(frame, order=order)
        else:
            lpc = np.vstack([lpc, pysptk.lpc(frame, order=order)])

    lpc[:, 0] = np.log(lpc[:, 0])
    return lpc


def toWave(x, fs, lpc, f0, output="synthed.wav", order=25, hopLength=80):

    generator = excite.ExcitePulse(fs, hopLength, False)
    sourceExcitation = generator.gen(f0)

    synthesizer = pysptk.synthesis.Synthesizer(pysptk.synthesis.AllPoleDF(order=order), hopLength)
    synthed = synthesizer.synthesis(sourceExcitation, lpc)
    wavfile.write(output, fs, synthed.astype(np.int16))


fs1, x1 = wavfile.read("sample/ssr1.wav")
fs2, x2 = wavfile.read("sample/tdm2.wav")
# x1 = x1[fs1//2:fs1//2+fs1]
# x2 = x2[fs2//2:fs2//2+fs2]

print(fs1, len(x1), len(x1) / fs1)
print(fs2, len(x2), len(x2) / fs2)

hopLength = 80
f01 = pysptk.swipe(x1.astype(np.float64), fs=fs1, hopsize=hopLength, min=50, max=500)
f02 = pysptk.swipe(x2.astype(np.float64), fs=fs2, hopsize=hopLength, min=50, max=500)

frames1 = toFrames(x1)
lpc1 = toLPC(frames1)

frames2 = toFrames(x2)
lpc2 = toLPC(frames2)

print(lpc1.shape, lpc2.shape)

toWave(x1, fs1, lpc2, f0=f02, output="synthed1.wav")
```

# 今後の予定
LPC使って変換するために学習モデルを構築する必要がある。
ので、とりあえず1次元畳み込みを使ってみようかな、と。
1チャンネルあたりが26とかの、ごく小規模なものだから効力が不安ではあるが、[MNISTを1次元畳み込みでやってる記事](https://agirobots.com/cnn1d-mnist/)があったので多分ダイジョブ。精度も悪く無さそうだし。

問題は、これを研究として発表なり論文投稿なりする場合、評価をどうするか。ただ聴き比べて「ほら、イイカンジでしょ？」はよろしくないらしい。学部のときに教授にこっぴどく言われた（卒研はそれで通してもらえたけど）。
なんか無いかねぇ。でもノンペアだしなぁ。LPCの類似度とか？PCA分析とかで圧縮して分布がどうなるか見る？
周波数分布でも何かできそうな気はするが。
まぁ、おいおい考えていきましょうかね。