短時間フーリエ変換
x軸が時間、y軸が振幅を表す波形にフーリエ変換(離散フーリエ変換)を施し、その絶対値をプロットするとx軸が周波数、y軸が強度を表す。
波形の全時間に対してどれくらいの周波数がどの程度含まれているのかを知りたい場合はこれで十分。
しかし、どの時間にどれくらいの周波数が含まれているかを知りたい場合には使えない。
それを解決する手法が短時間フーリエ変換(Short -Term Fourier Transformation, STFT)である。
こちらの説明が分かりやすい。
https://www.jstage.jst.go.jp/article/jasj/72/12/72_764/_pdf
通常のフーリエ変換に対して一定の区間だけで大きさを持つ窓関数を元の波形にかけてフーリエ変換を行う。
一度フーリエ変換が終われば窓関数を一定の幅だけスライドさせて元の波形にかけてフーリエ変換を行う。
これを繰り返すことにより時間と周波数、強度の3つの関係を表す図(スペクトログラム)を作成できる。
窓関数を元の波形に対して作用させると
が窓関数でその長さをとする。つまり、の範囲だけ値をとり、その他は0である。
このtは窓関数の時間座標から見たときの尺度なので、元の音源に対してはとなる。
ここでは窓関数をスライドさせる幅である。一度窓関数をスライドさせるごとにが1ずつ増加していく。
は窓関数を当てた後のスペクトログラムのフレーム数のインデックスに対応する。
これに対してフーリエ変換を施すので
となる。
この式から窓関数のサイズを大きく取るとより細かく周波数成分を調べることができる。
つまり周波数成分の分解能を高くすることができる。
周波数と時間の分解能にはトレードオフの関係があることに注意する。
wikiのExampleの比較も分かりやすい。
https://en.wikipedia.org/wiki/Short-time_Fourier_transform
スペクトログラムはx軸にを、y軸にをとってz軸にをプロットしたものになる。 3次元の図となるが基本的にはz軸を色の濃淡で表した2次元の図で表されることが通例である。
スペクトログラムもlibrosa
で簡単に作成できる。
import librosa import librosa.display import matplotlib.pyplot as plt sr = 44100 music, sr_ = librosa.load("fanfare.wav", sr=sr) D = np.abs(librosa.stft(y)) plt.figure(figsize=(10,4)) librosa.display.specshow(librosa.amplitude_to_db(D, ref=np.max), y_axis='log', x_axis='time', sr=sr) plt.title('Power spectrogram') plt.colorbar(format='%+2.0f dB') plt.tight_layout()
- 結果
(librosa.display.specshow
ではsr
を指定しないと意図しない値になるので注意)
D
の次元は(周波数、時間、強度)になっている。
n_fft
が窓関数のサイズ。上の数式で説明した長さに対応する。デフォルト値はn_fft=2048
。
hop_length
がスライドさせる幅。上の数式で説明した長さに対応する。デフォルト値はhop_length=n_fft/4
。
win_length
は窓関数の中でも実際に窓関数を適用させたい長さかな?基本的にはwin_length=n_fft
で、win_length<=n_fft
である。
大きさをn_fft
以下に設定した場合は0でパディングされる。
window
は窓関数の種類。デフォルトではhann
になっている。
n_fft
()をデフォルト値よりも大きくしてみる。
これは周波数成分の分解能を高くすることを意味している。
n_fft = 2048 * 2
で同様の計算をすると
以前と比較するとより正確に周波数成分が抽出されていることが確認できます。(横縞が見えやすくなっている。)
窓関数のサイズを2倍にしたので時間は半分になっています。
もっと大きくして、n_fft = 2048 * 8
でやると
より横縞がくっきり見えるようになりましたが、その分時間軸の境目の描写が曖昧になったような気がします。
この3つのスペクトルの配列を見てみると
n_fft=2048
: (1025, 1007)n_fft=2048 * 2
: (2049, 504)n_fft=2048 * 8
: (8193, 126)
となり、時間軸の刻み幅が小さくなっていくことがわかる。 時間の分解能が低く、周波数の分解能が高くなっていることを意味している。
デジタル波形について
仕事で音声処理をする機会があったので基本的な知識をまとめておく。
信号の種類
- アナログ信号:手を叩いて実際に我々の耳に入ってくる音(連続値)
- デジタル信号:手を叩いてマイクで集音しコンピュータ上で処理ができる音(離散値)
アナログ信号は連続値を取りますが、デジタル信号はアナログ信号をデジタル化(0と1で表す)したものであるので離散値を取ります。
波形のグラフはx軸に時間、縦軸にy幅をそれぞれ表します。
よく見るウニョウニョとした波の図です。
デジタル化
音のデジタル化は次の3つのステップを踏むようです。
- 標本化(サップリング):時間軸の刻み幅(細かさ)を決める
- 量子化:振幅の刻み幅を決める
- 符号化
・参考
https://www.infraexpert.com/study/telephony2.html
http://www.soraotona.net/weblog/?p=319
標本化(サンプリング、サンプリングレート)についてはこちらの説明が分かりやすいです。 http://www.aichi-c.ed.jp/contents/joho/contents/jissyuu/049/sound.files/oto.htm
以下は個人的な解釈、理解、要約です。
音の高さ(周波数)は1秒間の振動数で決まります。
振動数とは波形がx軸と交わる回数のことです。
よりx軸と多く交わっている波形の方が高い音になります。
サンプリングレートが低い場合は波形のx軸の刻み幅を大きくなってしまいます。
この場合、x軸と交わった回数を調べる精度が荒くなってしまうため、実際の信号の持つ周波数よりも低い周波数しか捉えることができなくなってしまいます。
ゆえにサンプリングレートを高くした方が高周波数の音を捉えることができ、実際の信号に近い音をデジタル化できるようです。
サンプリングレートを8kHzに設定すると1秒間に1/8000秒ごとの振幅データが得られます。
1秒の音源の場合のデータ数は8000個、2秒の場合は16000個になります。
逆にデータ数をサンプリングレートで割れば音源の再生時間が分かりますね。
サンプリング定理(Shannon-染谷の定理)によりサンプリングした音をアナログ信号に戻す場合の周波数は、サンプリングレートの0.5倍までしか復元できないようです。
8kHzでサンプリングした音をアナログ信号に復元すると4kHzになってしまいます。
なので最高周波数の2倍以上の周波数でサンプリングする必要があるようです。
(2点だけ波形の谷と山を観測できれば少なくとも1点はx軸と交わっているので、2倍以上のサンプリングをすれば良い的なロジックだったと思います。)
python上で音源の加工をする時はLibROSA
(https://librosa.github.io/librosa/)というモジュールが便利です。
短時間フーリエ変換を行い簡単にスペクトログラムを得ることもできます。
音源はこちらのfanfare.wav
を使用しました。
http://www.ne.jp/asahi/music/myuu/wave/wave.htm
%matplotlib inline import matplotlib.pyplot as plt import librosa import librosa.display sr = 44100 music, sr_ = librosa.load("fanfare.wav", sr=sr) print("play time : {} [sec]".format(len(music) / sr)) librosa.display.waveplot(music, sr=sr)
- 結果
play time : 11.679637188208616 [sec]
librosa.load
やlibrosa.display.waveplot
のsr(sampling rate)のデフォルト値が22050
なので注意してください。
何も指定しない場合はsr=22050
になるので波形のx軸の上限が実際の再生時間とは異なる値になってしまいます。
IPython上で音源を再生したい場合はIPython.display
を使用します。
https://musicinformationretrieval.com/ipython_audio.html
import IPython.display as ipd ipd.Audio(music, rate=sr)
無名関数(lambda)について
無名関数(lambda)について
sqr = lambda x : x**2
sqr(2)=4
が出力される。関数の引数は,
で指定すれば複数用いることが出来る。
myfunc = lambda x,y : x**2+y
出力結果
myfunc(2,3)=7
参考:
普通はsort
やmap
の引数で整理する時になどによく用いられる。
注意点はpython3ではmapがiteratorで返されるのでlist
で指定して返さねければならない。
import sys,osについて
ゼロから作るDeep Learning(p73)を読んでいてMNISTを読み込む時に使うsysとosについて。
osはファイルのパスを指定するモジュール
それぞれの使い方は↓を参考に。
os.pardirは現在のディレクトリより一つ上の階層のディレクトリの場所を指定する「".."」に相当する。
絶対パスを指定したい場合はos.listdirを用いる。
sys.path.appendはライブラリ読み込み時のパスを追加するために用いられる。