🔊

ボイスチェンジャー作るよ

2020/11/29に公開2

今日は皆さん。

このZennとやらが最近熱いということで、うちの神デザイナーやドラマー兼APEX大先生やTypeScript大好き兄貴とかが次々と参戦している中、割と古参PHPerである私が参戦しないというのはどうなのかと思い、PHPerの地位向上のためにもここはいっちょ記事の一つでも書いてみようじゃないかと考えた次第。
ちょうど自分好みの題材もゲットできたので、初記事と行きましょう。

今回はPythonの話です。

ボイチェン作りたい

スピーカーから突然聞きなれない声が聞こえてきたと思ったら、自分の声だったってのはよくあるのではと思います。
普段は骨伝導で自分の声を聴いているので、微妙に高めの声に聞こえて、「なんだこれ?」ってなるやつです。
なんというか、自分の声だとはわかるのだけど、少しだけ想定とずれているせいで気持ち悪さを感じると思うのですが、それならば思いっきりずらしてやれば、違和感を消すこともできるのでは?と考えた結果、ボイチェンでも作ってみようかなって思ったわけです。

ボイチェンを作る

大体の流れ

とりあえず声を変えることができるのかの検証を始めようかなと。今回はこんな感じです。

  • 声を録音したものをwaveに落とす
  • 録音した声を一定区間ごとに取り出し、フーリエ変換する
  • フーリエ変換で取得した波数の波形を高音方向に平行移動
  • 加工した波数グラフを元の音波に戻してつなげる

waveをフーリエ変換する方法はこれを見て下さいな。
https://qiita.com/niisan-tokyo/items/764acfeec77d8092eb73

声を取得

とりあえず声をゲットしなきゃならんので、適当なマイクを買ってきます( 現在の開発環境は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

catnosecatnose

素晴らしい記事をありがとうございます。タイトルが「ボスチェンジャー」になってます…!w