ボイスチェンジャー作るよ
今日は皆さん。
このZennとやらが最近熱いということで、うちの神デザイナーやドラマー兼APEX大先生やTypeScript大好き兄貴とかが次々と参戦している中、割と古参PHPerである私が参戦しないというのはどうなのかと思い、PHPerの地位向上のためにもここはいっちょ記事の一つでも書いてみようじゃないかと考えた次第。
ちょうど自分好みの題材もゲットできたので、初記事と行きましょう。
今回はPythonの話です。
ボイチェン作りたい
スピーカーから突然聞きなれない声が聞こえてきたと思ったら、自分の声だったってのはよくあるのではと思います。
普段は骨伝導で自分の声を聴いているので、微妙に高めの声に聞こえて、「なんだこれ?」ってなるやつです。
なんというか、自分の声だとはわかるのだけど、少しだけ想定とずれているせいで気持ち悪さを感じると思うのですが、それならば思いっきりずらしてやれば、違和感を消すこともできるのでは?と考えた結果、ボイチェンでも作ってみようかなって思ったわけです。
ボイチェンを作る
大体の流れ
とりあえず声を変えることができるのかの検証を始めようかなと。今回はこんな感じです。
- 声を録音したものをwaveに落とす
- 録音した声を一定区間ごとに取り出し、フーリエ変換する
- フーリエ変換で取得した波数の波形を高音方向に平行移動
- 加工した波数グラフを元の音波に戻してつなげる
waveをフーリエ変換する方法はこれを見て下さいな。
声を取得
とりあえず声をゲットしなきゃならんので、適当なマイクを買ってきます( 現在の開発環境はwindowsなのです )。
今回買ったのはUSBマイクでした。
普通に安いやつですね
で、これを使ってwindowsのボイスレコーダーとやらで保存。
ただ、保存したものがm4aファイルなので、wavに直すためにffmpeg使って変換しておきます。
docker run --rm -v /C/*******/Documents/***:/var jrottenberg/ffmpeg -i /var/niisan_voice.m4a /var/niisan_voice.wav
(windowsのpwd
だとpathがwindows形式になっちゃってホントめんどい。。)
Jupyter notebook
以下のDockerfileでJupyter notebookのコンテナを作って、基本的にはそこで作業します。
FROM python
RUN pip install numpy scipy matplotlib notebook
RUN mkdir /notebooks && mkdir /var/dev
CMD ["jupyter", "notebook", "--allow-root", "--ip=0.0.0.0", "--notebook-dir=/notebooks"]
オプション毎回つけるのめんどいので、docker-composeで適当に動かします。
version: "3"
services:
notebook:
image: niisan/python
volumes:
- ../notebooks:/notebooks
- ../src:/var/dev
- ../inputs:/var/inputs
ports:
- 8888:8888
フーリエ変換する
今回のマイクで録音した音声は、モノラルでした。
「こんにちは、niisan-tokyoです」と聞くに堪えない声でしゃべったデータでした。
import wave
import struct
from numpy import fromstring, int16
wavf = '/var/inputs/niisan_voice.wav'
wr = wave.open(wavf, 'r')
ch = wr.getnchannels()
width = wr.getsampwidth()
fr = wr.getframerate()
fn = wr.getnframes()
print("Channel: ", ch)
print("Sample width: ", width)
print("Frame Rate: ", fr)
print("Frame num: ", fn)
print("Total time: ", 1.0 * fn / fr)
結果としてはこんな感じです。
Channel: 1
Sample width: 2
Frame Rate: 48000
Frame num: 189440
Total time: 3.9466666666666668
なんでフレームレートが44.1kHzじゃないんやろ。。。
さて、このデータをフーリエ変換していきます。
フーリエ変換は一瞬で終わります。
# 3秒分データを持ってくる
sampleX = X[:fr * 3]
# 1/16 秒を一つの単位としてフーリエ変換する
span = fr // 16
# 3秒分のデータに対して、フーリエ変換をかける回数
num = fr * 3 // span
# 元データを変換 x[num][span] の形に変換
allWave = np.reshape(sampleX, (num, span))
# フーリエ変換
knums = np.fft.fft(allWave)
フーリエ変換するとこんな波形がゲットできます。
ボイチェンの実行
こうして得られた波形に対して平行移動の処理をします。
# 波数空間上でどの程度移動するか
move = 30
moveWave = np.zeros((num, move))
temp = knums
# 実際に平行移動
tempWave = np.concatenate((temp[:, :2], moveWave, temp[:, 2:]), 1)
平行移動時に、ヘンテコな小細工をしています(temp[:, :2], moveWave, temp[:, 2:]
の部分)が、これは超低波数のデータを一緒に動かすとグワングワンとハウリングするからです。なので、この部分は動かしません。
この操作でどうなったかというと、
先に出した波形に対して、右に少し移動していることがわかるでしょう。
こうして波形をほとんど変えずに音程を上げようとしているわけです。
あとはこいつを逆フーリエするだけです。
なお、フーリエ変換を実空間にかけると、実部が左右対称になり、虚部が点対称になります。
# 全波形の半分だけ持ってくる
temp = tempWave[:, :span//2]
# 波形を反転させたもの
tempi = temp[:,::-1]
# 虚部のスペクトルを反転させる
tempi.imag = -temp.imag[:,::-1]
# 平行移動済みの波形を形成する
movedWave = np.concatenate([temp, tempi], 1)
# 逆フーリエ変換( 実部だけ使う )
invF = np.fft.ifft(movedWave).real
resWave = np.reshape(invF, (-1)).astype(int16)
# 音声ファイルに保存
outd = struct.pack("h" * len(resWave), *resWave)
outf = '/var/inputs/test.wav'
ww = wave.open(outf, 'w')
ww.setnchannels(ch)
ww.setsampwidth(width)
ww.setframerate(fr)
ww.writeframes(outd)
ww.close()
これでボイチェンした音声が聞けるようになります。
結果
前
後
音声的には、曇りガラスの向こうでしゃべっている人のプライバシーを守るときの、独特の声が聞こえます。
かなり怖いです。
まとめ
というわけで、簡単にフーリエ変換使ってボイチェン作ってみました。
なんで作ったの?って言われても、作りたくなったからとしか言えないやつでした。
最近Python触っとらんかったので、思い出すにはちょうど良かったです。
今回はこんなところです。
おまけ
早回し
早回しにすると声が高くなりますが、あれは早回しにすることで音の周期が小さくなったため、結果的に高くなります。
波形データを利用して以下のように簡単に2倍速の状況を作ることができます。
newX = originalX[::2]
一応高くはなるのですが、同時に早回しになっちゃうので没です。
あと、倍率をうまく制御しにくいというのもマイナスです。
Discussion
素晴らしい記事をありがとうございます。タイトルが「ボスチェンジャー」になってます…!w
あっふ、失礼!