クーの自由研究

最下層からまた一段ずつ螺旋階段を登り始めます

フォルマント実験装置(ソフト)の製作(PyAudio編)

前のページの続きです

 

こんにちわ、こんばんわ、かえるのクーです。

 

前のページがだらだら作りながら書いていたら長くなったので、ページを改めました。

Qt5は情報があまりないので、そのような書き方でもよかったことにしておきます。

さて、一層だらだらと作りながら書いていきます。

PyAudio経由で音を出す

 いよいよ音を出す部分の実装にかかります。PyAudioの出番ですが、忘れてしまいました。自分のページで復習します。

実行しようとしたらpyaudioがない!と怒られました。

環境を最新にして、古いファイルをけしたので、PyAudioを消してしまったみたいです。pipします。

f:id:np2LKoo:20180512235251p:plain

現在の環境ではヘッドフォンをつないでいる、オーディオインターフェース番号は 2 であることが分かりました。

音を出す方法も完全に忘れてしまいました。これも上のページで復習します。

とりあえず、むりやり簡単にやってみます

さて、音声をリアルタイム処理するときは、「ストリーム」で行うことが基本になると思います。入力と出力で同期がとれる場合(入力と出力のスピードが同じ)は、ある程度バッファ(チャンク)を持ちながら、入力の都度、処理して(あるいはそのまま)出力すると思います。処理に少し時間がかかるようであれば、マルチスレッドやFIFO(ファースト・イン・ファーストアウト)を持ち出さないといけないかもしれません。

継続音はあとまわしにして、まずは所定の長さの音を出すように考えます。(それならマルチスレッドもFIFOもいらないはずです。


 (5/13はおでかけでしばし中断でした。内容的に別ページにしたくないので、このまま書き進めます。あしからず。)

さて、「音がでるだけ」のプログラムを作ってみましたが、どんなふうに倍音をかさねたらそれらしくなるのか、フォルマントの説明ページでなんとなくはわかりますが、実際にどんな値を指定すればいいのかがわかりません。

「Cevio」の「さとうささら」ちゃんにしゃべってもらいます。(この実験での合成ではありません。フォルマントを参照させてもらいます)

 1音を無理やり長く発音させたら、始まりや終わりが吐息っぽくなりました。ちょっと極端な感じですが、「吐息な感じ」は声には必須要素だと思っています。(あと、「ゆらぎ」もとっても大切だと勝手に思っています。)が・・・、今はsin波だけ注目します。

本当なら、フォルマントの包絡線計算をしてf0, f1 ... を導出して。。。となるのでしょうが、とりあえずは、フィルタ(もしくは倍音の構成)の「感じ」がつかめればOKとします。

「い」 「え」 「あ」

f:id:np2LKoo:20180514220511p:plain

f:id:np2LKoo:20180514221311p:plain

f:id:np2LKoo:20180514215345p:plain

「う」 「お」  

f:id:np2LKoo:20180514220600p:plain

f:id:np2LKoo:20180514221140p:plain

 

この周波数スペクトラム は、Audacityの「スペクトラム表示」で出したものです。

ささらちゃんは12倍音までを基本にして合成されていることがよくわかります。

ホワイトノイズの重ねかたも特徴的です。

f:id:np2LKoo:20180514223006j:plain

ノドの広がり具合や、口の形や舌の位置でできる口の中の空間を考えると、上のような周波数特性をもっているのがよく理解できます。

たとえば「い」の音は低いほうの音は基本周波数以外の第2~第5倍音くらいは極端に抑制され、第7~11倍音が協調されています。舌が口の前方で小さな空間を作り、高いほうの周波数のみを強調するのを感じながら発音してみましょう。

 さて、この周波数特性をまねて、sin波を合成してみましょう。


#coding: utf-8
import struct
import numpy as np
from pylab import *
import pyaudio


def play(data, fs):
import pyaudio
# ストリームを開く
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paInt16,
channels=1,
rate=int(fs),
output=True)
# チャンク単位でストリームに出力し音声を再生
chunk = 1024
sp = 0 # 再生位置ポインタ
buffer = data[sp:sp+chunk]
while buffer != b'':
stream.write(buffer)
sp = sp + chunk
buffer = data[sp:sp+chunk]
stream.close()
p.terminate()


def createFormant(formantList, fs, length):
"""振幅A、基本周波数f0、サンプリング周波数 fs、
長さlength秒の正弦波を作成して返す"""
data = []
ATACK_LENGTH = 4000.0
RELEASE_LENGTH = 8000.0
NO_TONE_LENGTH = 2048
for n in arange(length * fs):
s = 0.0
for fl in formantList:
ss = (fl[1] / 1000.0) * np.sin(2 * np.pi * fl[0] * n / fs)
# そのままだと耳が痛いので、アタックとリリースを丸めにしてみます。
if n < ATACK_LENGTH:
s += ss * (n / ATACK_LENGTH)
elif n > (length * fs - RELEASE_LENGTH - NO_TONE_LENGTH):
s += ss * max((length * fs - NO_TONE_LENGTH - n), 0) / RELEASE_LENGTH
else:
s += ss
data.append(s)

# 振幅が大きい時は正規化
datarange = max(max(data), -min(data))
# [-32768, 32767]の整数値に変換
data = [int(x * 32767.0 / datarange) for x in data]
# int 配列をバイナリストリーム列に変換
data = struct.pack("h" * len(data), *data) # この展開・変換方法はすごい!
return data


if __name__ == "__main__":
tone = 440.0 #基音 Hz
SAMPLING_RATE = 44100.0 #サンプリング周波数 Hz
TIME_LENGTH = 1.0 # Sec

# 各倍音の周波数は、サンプルの倍音実測(dB値)を振幅に変換して数値化(0dbで500、として-20dBで50換算の数値)
# 12倍音まで測定 指定の仕方は冗長ですが、Qt画面とつなぐためあえてこうしています。
formantList_a = [(tone * 1, 63), (tone * 2, 148), (tone * 3, 278), (tone * 4, 242),
(tone * 5, 54), (tone * 6, 22), (tone * 7, 19), (tone * 8, 54),
(tone * 9, 73), (tone * 10, 52), (tone * 11, 28), (tone * 12, 6)]
formantList_i = [(tone * 1, 199), (tone * 2, 3), (tone * 3, 3), (tone * 4, 1),
(tone * 5, 2), (tone * 6, 9), (tone * 7, 127), (tone * 8, 103),
(tone * 9, 177), (tone * 10, 130), (tone * 11, 96), (tone * 12, 20)]
formantList_u = [(tone * 1, 451), (tone * 2, 127), (tone * 3, 160), (tone * 4, 61),
(tone * 5, 6), (tone * 6, 1), (tone * 7, 1), (tone * 8, 1),
(tone * 9, 10), (tone * 10, 38), (tone * 11, 28), (tone * 12, 14)]
formantList_e = [(tone * 1, 123), (tone * 2, 153), (tone * 3, 29), (tone * 4, 23),
(tone * 5, 60), (tone * 6, 164), (tone * 7, 86), (tone * 8, 155),
(tone * 9, 99), (tone * 10, 73), (tone * 11, 64), (tone * 12, 16)]
formantList_o = [(tone * 1, 166), (tone * 2, 298), (tone * 3, 153), (tone * 4, 6),
(tone * 5, 2), (tone * 6, 2), (tone * 7, 3), (tone * 8, 23),
(tone * 9, 54), (tone * 10, 16), (tone * 11, 2), (tone * 12, 0)]
tone_a = createFormant(formantList_a, SAMPLING_RATE, TIME_LENGTH)
tone_i = createFormant(formantList_i, SAMPLING_RATE, TIME_LENGTH)
tone_u = createFormant(formantList_u, SAMPLING_RATE, TIME_LENGTH)
tone_e = createFormant(formantList_e, SAMPLING_RATE, TIME_LENGTH)
tone_o = createFormant(formantList_o, SAMPLING_RATE, TIME_LENGTH)
play(tone_a, SAMPLING_RATE)
play(tone_i, SAMPLING_RATE)
play(tone_u, SAMPLING_RATE)
play(tone_e, SAMPLING_RATE)
play(tone_o, SAMPLING_RATE)

 合成した母音音声はこんな風になりました。吐息成分や揺らぎがないので「あじけない」ですが、及第点はもらえる気がします。「ささら」ちゃんの声にちょっとだけ似ている感じがします。

 デシベルから振幅値を計算するにはExcelで

=10^(デシベル値/20) * 500

を小数点以下四捨五入して計算しました。例えばデシベル値が -10.6dBのときはプログラムの振幅値は148としています。(0dBで500を基準としました)倍音ピークのデシベル値は音の解析ソフト「Audacity」で簡単に分かります。

最終的には「声優ラジオ」から声優さんの声の特徴全般をリアルタイムに学習していき、ボクの音声を声優さんの声に変換しちゃう(自己)符号化器を妄想しています。

 

PyAudio編はここまでです。

ご視聴どうも有難うございました。

 

YouTuberになりたいかえる

  小学生に「ささら」ちゃんの声はバーチャル・YouTuberの声として結構つかわれていると教えてもらいました。「あ!、???や***の声と一緒だ!」と言われました。小学生恐るべし。いや、YouTubeばかりみてないで、おそとであそぼうよ。