備忘録も兼ねてるので結構雑なのは許してクレメンス。
これまでのお話
こんなことをやってたり、やっぱりダメだったのでCycleGan-VC2を参考にしたモデルで試したりしてた。
使ってた特徴量はメルケプストラム。この辺もCycleGAN-VC2を参考にして選択。
が、ダメ。
モデル調整を頑張ってみたり、メルケプストラムをNMFしてみたり。STFTしたもの(メルケプまで行かない)を入力してみたり、これもNMFしてみたり。なんか色々やったけど、終ぞ上手くいかず、そして萎えて断念。
今回のお話
断念してから1年。
ようやく新天地にたどり着いた。
前々から「LPCという特徴量がフィルタ的な役割でもある」とは聞いてた。音響管とかそういう話。
で、それを思い出し、調べ直した結果。このスライドの13枚め、「フォルマント合成 ~Formant Synthesis~」を見てビビッと来た。
フィルタってそういうことか
と。
こっからは早かった。
いそいそとpysptkをインストールし、チュートリアルとドキュメントを見ながらコーディング。
前に用意しておいたペアデータ音声からそれぞれLPCを求め、音声1にLPC2を、音声2にLPC1を与えて音声を再合成。
するとどうだろう。なんと声質が入れ替わって聞こえるではないか!
しかしまだ安心はできない。CycleGANを使うことを考えると、ノンペアデータでもそうなる必要がある。
ということで、発話内容が違い、話者も違う音声2つを用意して同様の実験を行ってみた。
結果、声質はおろか、発話内容も維持されているように聞こえるじゃないか。
こりゃもう勝ったな。風呂入ってくる。
ということで、大きな進展がありました。という話。
プログラム
音声読み込み
難しいことは無い。scipyのwavfileを使用する。
from scipy.io import wavfile
x, fs = wavfile.read("filename.wav")
ついでに、長さを揃える場合はこんな感じ。
x1 = x1[fs1//2:fs1//2+fs1]
音声のフレーム化
要は窓関数を掛けたりする。
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ループ使って実装。
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も求めておく。
hopLength = 80
f0 = pysptk.swipe(x.astype(np.float64), fs=fs, hopsize=hopLength, min=50, max=500)
LPCを音声に再合成
ここも基本的にはチュートリアルのまま。
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))
統合したもの
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次元畳み込みでやってる記事があったので多分ダイジョブ。精度も悪く無さそうだし。
問題は、これを研究として発表なり論文投稿なりする場合、評価をどうするか。ただ聴き比べて「ほら、イイカンジでしょ?」はよろしくないらしい。学部のときに教授にこっぴどく言われた(卒研はそれで通してもらえたけど)。
なんか無いかねぇ。でもノンペアだしなぁ。LPCの類似度とか?PCA分析とかで圧縮して分布がどうなるか見る?
周波数分布でも何かできそうな気はするが。
まぁ、おいおい考えていきましょうかね。