前回の記事で、リアル音声処理の下準備として、オーバーラップ+窓関数を実装しました。
今回は、FFTで周波数データに変換した後、音声のキーを変更し、元に戻してみたいと思います。
ピー音の音声処理から始まり、ようやくここまでたどり着きました。十分15年前に味わった挫折のリベンジを果たせたと思っております。
それでは、早速プログラム全体を見てみましょう。
# -*- coding: utf-8 -*- | |
import wave | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import struct | |
# 周波数シフト関数 | |
def shift_freq(F, s_key, fs, N): | |
# 半音倍数 | |
n = 1.0594623228711 | |
s_freq = s_key * n * (N / fs) | |
# プラス方向へのシフト | |
if s_freq > 0: | |
# 前半部分のシフト | |
for i in reversed(range(0,int(N/2))): | |
si = (int)(i - i * s_freq) | |
if si >= 0: | |
F.real[i] = F.real[si] | |
F.imag[i] = F.imag[si] | |
elif si < 0: | |
F.real[i] = 0 | |
F.imag[i] = 0 | |
# 後半部分のシフト | |
for i in range(int(N/2)+1,N): | |
si = (int)(i + i * s_freq) | |
if si < N: | |
F.real[i] = F.real[si] | |
F.imag[i] = F.imag[si] | |
elif si >= N: | |
F.real[i] = 0 | |
F.imag[i] = 0 | |
# マイナス方向へのシフト | |
elif s_freq < 0: | |
# 前半部分のシフト | |
for i in range(0,int(N/2)): | |
si = (int)(i - i * s_freq) | |
if si < int(N/2): | |
F.real[i] = F.real[si] | |
F.imag[i] = F.imag[si] | |
elif si >= int(N/2): | |
F.real[i] = 0 | |
F.imag[i] = 0 | |
# 後半部分のシフト | |
for i in reversed(range(int(N/2)+1,N)): | |
si = (int)(i + i * s_freq) | |
if si > int(N/2)+1: | |
F.real[i] = F.real[si] | |
F.imag[i] = F.imag[si] | |
elif si < int(N/2)+1: | |
F.real[i] = 0 | |
F.imag[i] = 0 | |
return F | |
# Wave読み込み | |
def read_wav(file_path): | |
wf = wave.open(file_path, "rb") | |
buf = wf.readframes(-1) # 全部読み込む | |
# 2なら16bit,4なら32bitごとに10進数化 | |
if wf.getsampwidth() == 2: | |
data = np.frombuffer(buf, dtype='int16') | |
elif wf.getsampwidth() == 4: | |
data = np.frombuffer(buf, dtype='int32') | |
# ステレオの場合,チャンネルを分離 | |
if wf.getnchannels()==2: | |
data_l = data[::2] | |
data_r = data[1::2] | |
else: | |
data_l = data | |
data_r = data | |
wf.close() | |
return data_l,data_r | |
# wavファイルの情報を取得 | |
def getWavInfo(file_path): | |
ret = {} | |
wf = wave.open(file_path, "rb") | |
ret["ch"] = wf.getnchannels() | |
ret["byte"] = wf.getsampwidth() | |
ret["fs"] = wf.getframerate() | |
ret["N"] = wf.getnframes() | |
ret["sec"] = ret["N"] / ret["fs"] | |
wf.close() | |
return ret | |
# wavファイル書き込み | |
def write_wav(file_path, data_l, data_r, wav_info): | |
if wav_info["ch"] == 2: | |
data = np.array([[int(data_l[i]), int(data_r[i])] for i in range(int(wav_info["N"]))]).flatten() | |
else: | |
data = np.array([int(x) for x in data_l]) | |
# 音量調整 | |
amp = 32767 / max(data) | |
data = np.array([int(x * amp) for x in data]) | |
# 最大値と最小値に収める | |
data = np.clip(data, -32768, 32767) | |
# バイナリ化 | |
binwave = struct.pack("h" * len(data), *data) | |
# wavファイルとして書き出し | |
w = wave.Wave_write(file_path) | |
p = (wav_info["ch"], wav_info["byte"], wav_info["fs"], wav_info["N"], 'NONE', 'not compressed') | |
w.setparams(p) | |
w.writeframes(binwave) | |
w.close() | |
# オーバーラップ+窓関数 | |
def half_overlap_win(data_l, data_r, wi, win_size): | |
ret_ls = [] | |
ret_rs = [] | |
win = np.hanning(win_size) | |
for i in range(0, wi["N"] ,int(win_size/2)): | |
endi = i + win_size | |
if endi < wi["N"]: | |
ret_ls.append(data_l[i:endi] * win) | |
ret_rs.append(data_r[i:endi] * win) | |
else: | |
win = np.hanning(len(data_l[i:-1])) | |
ret_ls.append(data_l[i:-1] * win) | |
ret_rs.append(data_r[i:-1] * win) | |
return ret_ls,ret_rs | |
# オーバラップ+窓関数の音声を元に戻す | |
def return_how(data_ls, data_rs, wi, win_size): | |
ret_l = np.zeros(wi["N"]) | |
ret_r = np.zeros(wi["N"]) | |
for i in range(0, len(data_ls)): | |
to = win_size | |
to = to if to <= len(data_ls[i].real) else len(data_ls[i].real) | |
for j in range(0, to): | |
ret_l[i*int(win_size/2)+j] += data_ls[i].real[j] | |
ret_r[i*int(win_size/2)+j] += data_rs[i].real[j] | |
return ret_l,ret_r | |
if __name__ == '__main__': | |
# 音声データパス | |
onsei_path = "arigato.wav" | |
# 出力音声パス | |
out_onsei_path = "i_arigato2.wav" | |
# ウィンドウサイズ | |
win_size = 1024 * 2 | |
# キー変更 | |
shift_f = -8 | |
# 音声読み込み | |
data_l,data_r = read_wav(onsei_path) | |
# Wavの情報取得 | |
wi = getWavInfo(onsei_path) | |
# オーバーラップ+窓関数 | |
data_ls,data_rs = half_overlap_win(data_l, data_r, wi, win_size) | |
# FFT | |
F_ls = [] | |
F_rs = [] | |
for dl in data_ls: | |
F_ls.append(np.fft.fft(dl)) | |
for dr in data_rs: | |
F_rs.append(np.fft.fft(dr)) | |
# 周波数変更 | |
sF_ls = [] | |
sF_rs = [] | |
for F_l in F_ls: | |
sF_ls.append(shift_freq(F_l, shift_f, wi["fs"], len(F_l.real))) | |
for F_r in F_rs: | |
sF_rs.append(shift_freq(F_r, shift_f, wi["fs"], len(F_r.real))) | |
# 逆FFT | |
IF_ls = [] | |
IF_rs = [] | |
for fl in sF_ls: | |
IF_ls.append(np.fft.ifft(fl)) | |
for fr in sF_rs: | |
IF_rs.append(np.fft.ifft(fr)) | |
# オーバーラップ+窓関数を元に戻す | |
ret_l,ret_r = return_how(IF_ls, IF_rs, wi, win_size) | |
# wav書き出し | |
write_wav(out_onsei_path, ret_l, ret_r, wi) |
プログラムの流れは、前回記事とほぼ同様ですが、FFT処理と逆FFT処理の間に、キー変更の処理を施しています。
そして、キー変更の関数を定義しています。その部分を解説していきます。
今回実装した周波数変更処理は、Pythonで音声処理(3)で実装した周波数変更処理がベースになっています。
前回からの変更点は、Pythonで音声処理(4)で示した通り、キー変更は周波数の足し算ではなく、掛け算となる点です。
具体的に見ていきましょう。
8行目で、キー変更関数「shift_freq」の定義を開始しています。引数として、FFT後のデータ、キーをいくつ上下させるか(下げる場合はマイナス)、サンプリング周波数、FFT後のデータ数をインプットしています。
10行目で、半音倍数を定義しています。これは、キーが一つ上がる(半音上がる)場合に、周波数が何倍になるかを表しています。
11行目で、キー、サンプリング周波数、ウインドウサイズに応じた実際にシフトさせる幅を決めるための係数(シフト係数)を計算しています。
13行目以降の処理は、Pythonで音声処理(3)のプログラムとほぼ同一ですが、16行目、25行目、36行目、45行目が異なります。Pythonで音声処理(3)では、単純にシフト分を加減算してましたが、今回は、インデックスにシフト係数を乗算したものを加減算しています。
これにより、音声中のどの周波数においても、同一のキー変更が適用されます。
それでは、リアル音声に対して、本プログラムを実行していきましょう。
入力する音声は、前回と同じ「arigato.wav」です。音量にご注意ください。
今回のメイン処理では、キーを「-8」としています。女性の音声であれば、男性の音声に変換されるはずです。
喋り方が女性っぽいので、違和感がありますが、声が低くなったことが確認できます。
続いて、音声のキーを高くしてみます。
音声を探していましたが、ちょうど良いものを見つけました。
筆者がカラオケで録音したあいみょんの「君はロックを聴かない」です。
これは原曲キーからキーを4つ下げて歌っているため、こちらを「+4」にして原曲キーにしてみます。
こちらも音量にご注意ください。
はい。熱唱していますね。笑。
これを今回作成したプログラムで、キーを+4にすると次のようになります。
うーん。少し音質が悪いのが気になりますが、原曲キーになっているかなと思います。
全6回に渡って掲載してきた、Pythonで音声処理は、今回の記事をもって一旦完結とします。
音声処理の次は、やはり画像処理をやってみようと思います。
コメント