前回の記事では、リアル音声の周波数をグラフ化し、リアル音声処理を実施する上での課題について説明しました。
今回は、リアル音声を周波数解析する上で必要となる、オーバーラップ処理と窓関数の実装について、解説していきたいと思います。
今回作成するプログラムの流れは、次のようにします。
1.音声を読み込む。
2.オーバーラップ+窓関数処理を行い、一定時間ごとの音声データ群を作成する。
3.各音声データに対してFFT処理をかけて、周波数データ群を作成する。
4.周波数データ群に逆FFT処理をかけて、音声データ群に戻す。
5.音声データ群を元の音声に戻す。
一定時間ごとの周波数データに変換して、何もせずに元の音声データに戻すプログラムです。
このプログラムができてしまえば、あとは自由に周波数解析が可能になります。
では、早速プログラム全体を見ていきましょう。
# -*- coding: utf-8 -*- | |
import wave | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import struct | |
# 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_arigato.wav" | |
# ウィンドウサイズ | |
win_size = 1024 * 2 | |
# 音声読み込み | |
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)) | |
# 逆FFT | |
IF_ls = [] | |
IF_rs = [] | |
for fl in F_ls: | |
IF_ls.append(np.fft.ifft(fl)) | |
for fr in F_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) |
少し長いですが、やっていることは上記の通りです。
今回追加したオーバーラップ+窓関数処理を見ていきましょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# オーバーラップ+窓関数
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
61行目で、オーバーラップ+窓関数処理の関数「half_overlap_win」の定義を開始しています。一定時間毎の音声データを作成しますが、半分をオーバーラップさせるため「half」という名前をつけています。引数として、音声データのL、RそれぞれのデータとWavファイルの情報、切り取る大きさ(ウインドウサイズ)をインプットします。
62、63行目では、オーバーラップ+窓関数処理をかけたあとの音声データをリストとして返すための戻り値配列を作成しています。
64行目では、ウインドウサイズに合った窓関数を定義しています。窓関数は最も一般的なハニング窓を利用します。
65行目〜73行目のループで、音声信号を切り取り配列に格納していきます。
65行目は、forループのループ方法を定義しています。iを0からウインドウサイズの半分ずつ加えていき、音声の最後になるまでループします。
66行目で、endi=i+ウインドウサイズを計算しています。
67行目のif文は、endiが音声データの最後を超えてしまう場合があるので、超えていない場合と超える場合で処理を分けています。68行目、69行目は超えない場合の処理です。
68、69行目で、i〜endiで音声データを切り取り、ハニング窓を乗算した上で、戻り値配列に追加しています。
70行目〜73行目は、endiが音声データの最後を超えてしまう場合の処理です。この場合、ハニング窓の大きさも変わってくるので、71行目で再定義します。
72、73行目で、i〜音声データの最後までを切り取り、ハニング窓を乗算した上で、戻り値配列に追加しています。
74行目で、LとRそれぞれのデータを戻り値として返しています。
続いて、切り取った音声データを元の音声データに戻す関数定義について見ていきましょう。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# オーバラップ+窓関数の音声を元に戻す
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
77行目で、切り取った音声データを元の音声データに戻す関数「return_how」の定義を開始しています。「how」は「half_overlap_win」の略です。引数として、LとRそれぞれの配列とWavファイルの情報、ウインドウサイズをインプットします。
78、79行目で、LとRそれぞれの元音声データを入れるための変数を定義しています。オーバーラップしたデータを加えるため、データは予めゼロ埋めしておきます。
80行目でforループを定義しています。iを0から配列の最大サイズまでループします。ループ内ではLとRの処理を行いますが、LとRの配列サイズは同じため、Lの配列で最大サイズを取得しています。
81行目で、toにウィンドウサイズを格納していますが、配列の最後には、ウィンドウサイズ未満のデータがあるので、82行目の三項演算子により、ウィンドウサイズ未満の場合は、toに配列サイズを入れるようにします。
83行目〜85行目は、切り取った音声データの中で更にjが0からtoになるまでループします。
84、85行目で、LとRそれぞれのデータに対して、i×ウィンドウサイズの半分+jの位置に、音声データを足し込んでいきます。
86行目で、LとRそれぞれで復元した音声データを返して処理完了です。
それでは、プログラムを実行して見ましょう。
入力する音声は、前回の記事と同様「arigato.wav」です。
この音声データを本プログラムで処理して得られた「i_arigato.wav」が以下になります。
色々と処理を施したにも関わらず、ほとんど何も変わっていないことがわかると思います。
これで、周波数変更のための下準備が整いました。
次回は本プログラムに、周波数変更処理を追加して、音声処理を行いたいと思います。
コメント